Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
94be2b46d5 | |||
4b4d0d2d19 | |||
0d06cf63b7 | |||
7b24c02d51 | |||
e89e8226e4 | |||
a533a96598 | |||
27321c0919 | |||
058472d325 | |||
b5c9a03052 | |||
07dad3affa | |||
8afc103ae7 | |||
591d7b4b80 | |||
2162afc78e | |||
25e226d219 | |||
8472bfe90d | |||
93645b2fbe | |||
d53c987f2e | |||
682693a9f0 | |||
e836faf792 | |||
6e27233be8 | |||
9209984a2f | |||
1477630c78 |
@ -12,6 +12,7 @@ jobs:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
echo "117.18.232.200 api.nuget.org" | sudo tee -a /etc/hosts
|
||||
lsb_release -a
|
||||
wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
@ -35,7 +36,7 @@ jobs:
|
||||
command: |
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
|
||||
sudo docker build --add-host "api.nuget.org:117.18.232.200" --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
|
||||
|
||||
@ -49,7 +50,7 @@ jobs:
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
|
||||
sudo docker build --add-host "api.nuget.org:117.18.232.200" --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||
|
||||
|
@ -48,6 +48,7 @@ using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using NBXplorer.Models;
|
||||
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
|
||||
using NBitpayClient.Extensions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -350,57 +351,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.False(invoice.Refundable);
|
||||
|
||||
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
|
||||
var request = url.GetPaymentRequest();
|
||||
var payment = request.CreatePayment();
|
||||
|
||||
Transaction tx = new Transaction();
|
||||
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
|
||||
var cashCow = tester.ExplorerNode;
|
||||
tx = cashCow.FundRawTransaction(tx).Transaction;
|
||||
tx = cashCow.SignRawTransaction(tx);
|
||||
|
||||
payment.Transactions.Add(tx);
|
||||
|
||||
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
|
||||
var ack = payment.SubmitPayment();
|
||||
Assert.NotNull(ack);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.True(localInvoice.Refundable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetLightningServer()
|
||||
@ -934,6 +884,17 @@ namespace BTCPayServer.Tests
|
||||
var result = client.SendAsync(message).GetAwaiter().GetResult();
|
||||
result.EnsureSuccessStatusCode();
|
||||
/////////////////////
|
||||
|
||||
// Have error 403 with bad signature
|
||||
client = new HttpClient();
|
||||
HttpRequestMessage mess = new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens");
|
||||
mess.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
|
||||
mess.Headers.Add("x-signature", "3045022100caa123193afc22ef93d9c6b358debce6897c09dd9869fe6fe029c9cb43623fac022000b90c65c50ba8bbbc6ebee8878abe5659e17b9f2e1b27d95eda4423da5608fe");
|
||||
mess.Headers.Add("x-identity", "04b4d82095947262dd70f94c0a0e005ec3916e3f5f2181c176b8b22a52db22a8c436c4703f43a9e8884104854a11e1eb30df8fdf116e283807a1f1b8fe4c182b99");
|
||||
mess.Method = HttpMethod.Get;
|
||||
result = client.SendAsync(mess).GetAwaiter().GetResult();
|
||||
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, result.StatusCode);
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@ -1801,7 +1762,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate
|
||||
Assert.True(IsMapped(invoice, ctx));
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
|
||||
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
|
||||
var invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
|
||||
Assert.Single(invoiceEntity.HistoricalAddresses);
|
||||
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
|
||||
|
||||
@ -1819,7 +1780,7 @@ ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate
|
||||
Assert.True(IsMapped(invoice, ctx));
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
|
||||
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
|
||||
invoiceEntity = repo.GetInvoice(invoice.Id, true).GetAwaiter().GetResult();
|
||||
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
|
||||
Assert.NotNull(historical1.UnAssigned);
|
||||
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
|
||||
|
@ -242,6 +242,8 @@ namespace BTCPayServer.Authentication
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
if (token == null)
|
||||
return null;
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.24</Version>
|
||||
<Version>1.0.3.28</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -33,7 +33,7 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.3" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.20" />
|
||||
@ -46,7 +46,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.72" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.1" />
|
||||
@ -136,9 +136,6 @@
|
||||
<Content Update="Views\Home\BitpayTranslator.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndRestServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
@ -154,7 +151,7 @@
|
||||
<Content Update="Views\Public\PayButtonHandle.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\LndGrpcServices.cshtml">
|
||||
<Content Update="Views\Server\LndServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\Maintenance.cshtml">
|
||||
|
@ -141,6 +141,18 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
var services = conf.GetOrDefault<string>("externalservices", null);
|
||||
if(services != null)
|
||||
{
|
||||
foreach(var service in services.Split(new[] { ';', ',' })
|
||||
.Select(p => p.Split(':'))
|
||||
.Where(p => p.Length == 2)
|
||||
.Select(p => (Name: p[0], Link: p[1])))
|
||||
{
|
||||
ExternalServices.AddOrReplace(service.Name, service.Link);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
@ -248,6 +260,8 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
|
@ -33,6 +33,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
@ -32,6 +33,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
{
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
|
||||
|
@ -50,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new Exception("This tool only work with bitpay");
|
||||
@ -57,6 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var client = HttpClientFactory.CreateClient();
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request"));
|
||||
var result = await client.SendAsync(request);
|
||||
// {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"}
|
||||
|
@ -40,16 +40,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = id,
|
||||
StoreId = new[] { HttpContext.GetStoreData().Id }
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
|
@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
|
||||
if (store.StoreCertificate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Sign(store.StoreCertificate, PKIType.X509SHA256);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, "Error while signing payment request");
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentRequestActionResult(request);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}", Order = 99)]
|
||||
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
|
||||
[MediaTypeConstraint("application/bitcoin-payment")]
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PaymentRequestActionResult : IActionResult
|
||||
{
|
||||
PaymentRequest req;
|
||||
public PaymentRequestActionResult(PaymentRequest req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PaymentAckActionResult : IActionResult
|
||||
{
|
||||
PaymentACK req;
|
||||
public PaymentAckActionResult(PaymentACK req)
|
||||
{
|
||||
this.req = req;
|
||||
}
|
||||
public Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
|
||||
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
|
||||
req.WriteTo(context.HttpContext.Response.Body);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -30,11 +31,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId(),
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
@ -48,7 +51,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
State = invoice.GetInvoiceState().ToString(),
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
@ -209,7 +212,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
@ -298,7 +301,9 @@ namespace BTCPayServer.Controllers
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
ChangellyEnabled = changelly != null,
|
||||
@ -369,8 +374,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -437,15 +442,18 @@ namespace BTCPayServer.Controllers
|
||||
var list = await ListInvoicesProcess(searchTerm, skip, count);
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
ShowCheckout = invoice.Status == "new",
|
||||
Status = state.ToString(),
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete()
|
||||
});
|
||||
}
|
||||
return View(model);
|
||||
@ -585,17 +593,60 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/invalidatepaid")]
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (newState == "invalid")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice invalid",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?",
|
||||
});
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice complete",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
}
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ChangeInvoiceStateConfirm(string invoiceId, string newState)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId()
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete"));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
|
@ -435,12 +435,27 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
}
|
||||
result.HasSSH = _Options.SSHSettings != null;
|
||||
foreach(var externalService in _Options.ExternalServices)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
{
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetRelativePath(externalService.Value)
|
||||
});
|
||||
}
|
||||
if(_Options.SSHSettings != null)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
public IActionResult LndGrpcServices(string cryptoCode, int index, uint? nonce)
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
public IActionResult LndServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
@ -451,9 +466,18 @@ namespace BTCPayServer.Controllers
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
}
|
||||
else if(external.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
model.Uri = external.BaseUri.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
if (external.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
@ -462,10 +486,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
}
|
||||
if (external.RestrictedMacaroon != null)
|
||||
{
|
||||
model.RestrictedMacaroon = Encoders.Hex.EncodeData(external.RestrictedMacaroon);
|
||||
}
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
@ -492,29 +520,44 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-grpc/{cryptoCode}/{index}")]
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
[HttpPost]
|
||||
public IActionResult LndGrpcServicesPost(string cryptoCode, int index)
|
||||
public IActionResult LndServicesPost(string cryptoCode, int index)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.ChainType = _Options.NetworkType.ToString();
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
LightningConfiguration conf = new LightningConfiguration();
|
||||
conf.Type = "grpc";
|
||||
conf.ChainType = _Options.NetworkType.ToString();
|
||||
conf.CryptoCode = cryptoCode;
|
||||
conf.Host = external.BaseUri.DnsSafeHost;
|
||||
conf.Port = external.BaseUri.Port;
|
||||
conf.SSL = external.BaseUri.Scheme == "https";
|
||||
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
conf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon);
|
||||
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(conf);
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.ChainType = _Options.NetworkType.ToString();
|
||||
restconf.CryptoCode = cryptoCode;
|
||||
restconf.Uri = external.BaseUri.AbsoluteUri;
|
||||
restconf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
restconf.RestrictedMacaroon = external.RestrictedMacaroon == null ? null : Encoders.Hex.EncodeData(external.RestrictedMacaroon);
|
||||
restconf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd-grpc", cryptoCode, index, nonce);
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndGrpcServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
|
||||
@ -536,29 +579,19 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
[Route("server/services/lnd-rest/{cryptoCode}/{index}")]
|
||||
public IActionResult LndRestServices(string cryptoCode, int index, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
if (connectionString.RestrictedMacaroonFilePath != null)
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
try
|
||||
{
|
||||
connectionString.RestrictedMacaroon = System.IO.File.ReadAllBytes(connectionString.RestrictedMacaroonFilePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{cryptoCode}: The restrictedmacaroon file path of the external LND grpc config was not found ({connectionString.RestrictedMacaroonFilePath})");
|
||||
}
|
||||
connectionString.RestrictedMacaroonFilePath = null;
|
||||
}
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LndRestServicesViewModel();
|
||||
|
||||
model.BaseApiUrl = external.BaseUri.ToString();
|
||||
if (external.CertificateThumbprint != null)
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
if (external.Macaroon != null)
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
|
||||
return View(model);
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
|
@ -81,5 +81,10 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
|
||||
public Services.Invoices.InvoiceState GetInvoiceState()
|
||||
{
|
||||
return new Services.Invoices.InvoiceState(Status, ExceptionStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,23 +11,14 @@ namespace BTCPayServer.Events
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
State = invoice.GetInvoiceState();
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
public string InvoiceId { get; }
|
||||
public InvoiceState State { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
return $"Invoice status is {State}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,15 @@ namespace BTCPayServer
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetRelativePath(this HttpRequest request, string path)
|
||||
{
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
path);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
|
@ -330,7 +330,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(e.Invoice.Id);
|
||||
if (invoice == null)
|
||||
return;
|
||||
List<Task> tasks = new List<Task>();
|
||||
@ -345,6 +345,7 @@ namespace BTCPayServer.HostedServices
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_markedComplete" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed" ||
|
||||
e.Name == "invoice_expiredPaidPartial"
|
||||
|
@ -61,14 +61,14 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
if(invoice.ExceptionStatus == "paidPartial")
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
}
|
||||
|
||||
@ -78,57 +78,57 @@ namespace BTCPayServer.HostedServices
|
||||
if (paymentMethod == null)
|
||||
return;
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
if (invoice.Status == InvoiceStatus.New || invoice.Status == InvoiceStatus.Expired)
|
||||
{
|
||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
if (invoice.Status == InvoiceStatus.Paid)
|
||||
{
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.None;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
if (accounting.Paid > accounting.TotalDue && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidOver;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
invoice.Status = InvoiceStatus.New;
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? InvoiceExceptionStatus.None : InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
if (invoice.Status == InvoiceStatus.Paid)
|
||||
{
|
||||
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
|
||||
|
||||
@ -140,25 +140,25 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
if (invoice.Status == InvoiceStatus.Confirmed)
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@ -208,7 +208,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
try
|
||||
{
|
||||
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
@ -283,14 +283,14 @@ namespace BTCPayServer.HostedServices
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
@ -299,8 +299,8 @@ namespace BTCPayServer.HostedServices
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
|
@ -81,11 +81,11 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string BOLT11 { get; set; }
|
||||
}
|
||||
|
||||
public string Status
|
||||
public string State
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StatusException { get; set; }
|
||||
public InvoiceExceptionStatus StatusException { get; set; }
|
||||
public DateTimeOffset CreatedDate
|
||||
{
|
||||
get; set;
|
||||
|
@ -46,6 +46,9 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool CanMarkComplete { get; set; }
|
||||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
|
||||
public bool ShowCheckout { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
public string AmountCurrency
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -10,8 +11,12 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
public string Host { get; set; }
|
||||
public bool SSL { get; set; }
|
||||
public string Macaroon { get; set; }
|
||||
public string RestrictedMacaroon { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string QRCode { get; set; }
|
||||
public string QRCodeLink { get; set; }
|
||||
[Display(Name = "REST Uri")]
|
||||
public string Uri { get; set; }
|
||||
public string ConnectionType { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,13 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
|
||||
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
|
||||
public bool HasSSH { get; set; }
|
||||
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, false);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
@ -315,7 +315,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var invoices = await _InvoiceRepository.GetPendingInvoices();
|
||||
foreach (var invoiceId in invoices)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
continue;
|
||||
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
|
||||
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (Listening(invoiceId))
|
||||
return;
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
{
|
||||
@ -194,7 +194,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}, network.CryptoCode, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
|
||||
if (invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
|
@ -28,11 +28,12 @@
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
}
|
||||
|
@ -57,39 +57,37 @@ namespace BTCPayServer.Security
|
||||
List<Claim> claims = new List<Claim>();
|
||||
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
// Careful, those are not the opposite. failedAuth says if a the tentative failed.
|
||||
// successAuth, ensure that at least one succeed.
|
||||
var failedAuth = false;
|
||||
var successAuth = false;
|
||||
|
||||
bool? success = null;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
|
||||
storeId = result.StoreId;
|
||||
successAuth = result.SuccessAuth;
|
||||
failedAuth = !successAuth;
|
||||
success = result.SuccessAuth;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
|
||||
successAuth = storeId != null;
|
||||
failedAuth = !successAuth;
|
||||
success = storeId != null;
|
||||
}
|
||||
|
||||
if (failedAuth)
|
||||
if (success.HasValue)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
}
|
||||
|
||||
if (successAuth)
|
||||
{
|
||||
if (storeId != null)
|
||||
if (success.Value)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
store.AdditionalClaims.AddRange(claims);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
if (storeId != null)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
store.AdditionalClaims.AddRange(claims);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
else
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
}
|
||||
return AuthenticateResult.NoResult();
|
||||
|
@ -77,7 +77,9 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
CreatedDate = invoice.InvoiceTime.UtcDateTime,
|
||||
ExpirationDate = invoice.ExpirationTime.UtcDateTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration.UtcDateTime,
|
||||
Status = invoice.Status,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
ItemCode = invoice.ProductInformation?.ItemCode,
|
||||
ItemDesc = invoice.ProductInformation?.ItemDesc,
|
||||
FiatPrice = invoice.ProductInformation?.Price ?? 0,
|
||||
|
@ -226,15 +226,23 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public string Status
|
||||
[JsonIgnore]
|
||||
public InvoiceStatus Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string ExceptionStatus
|
||||
[JsonProperty(PropertyName = "status")]
|
||||
[Obsolete("Use Status instead")]
|
||||
public string StatusString => InvoiceState.ToString(Status);
|
||||
[JsonIgnore]
|
||||
public InvoiceExceptionStatus ExceptionStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[JsonProperty(PropertyName = "exceptionStatus")]
|
||||
[Obsolete("Use ExceptionStatus instead")]
|
||||
public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus);
|
||||
|
||||
[Obsolete("Use GetPayments instead")]
|
||||
public List<PaymentEntity> Payments
|
||||
@ -341,7 +349,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
InvoiceTime = InvoiceTime,
|
||||
ExpirationTime = ExpirationTime,
|
||||
Status = Status,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = StatusString,
|
||||
ExceptionStatus = ExceptionStatus == InvoiceExceptionStatus.None ? new JValue(false) : new JValue(ExceptionStatusString),
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Currency = ProductInformation.Currency,
|
||||
Flags = new Flags() { Refundable = Refundable },
|
||||
PaymentSubtotals = new Dictionary<string, long>(),
|
||||
@ -395,9 +406,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
@ -450,7 +458,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
|
||||
dto.Guid = Guid.NewGuid().ToString();
|
||||
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@ -529,6 +536,108 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public InvoiceState GetInvoiceState()
|
||||
{
|
||||
return new InvoiceState(Status, ExceptionStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
New,
|
||||
Paid,
|
||||
Expired,
|
||||
Invalid,
|
||||
Complete,
|
||||
Confirmed
|
||||
}
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
public class InvoiceState
|
||||
{
|
||||
static Dictionary<string, InvoiceStatus> _StringToInvoiceStatus;
|
||||
static Dictionary<InvoiceStatus, string> _InvoiceStatusToString;
|
||||
|
||||
static Dictionary<string, InvoiceExceptionStatus> _StringToExceptionStatus;
|
||||
static Dictionary<InvoiceExceptionStatus, string> _ExceptionStatusToString;
|
||||
|
||||
static InvoiceState()
|
||||
{
|
||||
_StringToInvoiceStatus = new Dictionary<string, InvoiceStatus>();
|
||||
_StringToInvoiceStatus.Add("paid", InvoiceStatus.Paid);
|
||||
_StringToInvoiceStatus.Add("expired", InvoiceStatus.Expired);
|
||||
_StringToInvoiceStatus.Add("invalid", InvoiceStatus.Invalid);
|
||||
_StringToInvoiceStatus.Add("complete", InvoiceStatus.Complete);
|
||||
_StringToInvoiceStatus.Add("new", InvoiceStatus.New);
|
||||
_StringToInvoiceStatus.Add("confirmed", InvoiceStatus.Confirmed);
|
||||
_InvoiceStatusToString = _StringToInvoiceStatus.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
|
||||
_StringToExceptionStatus = new Dictionary<string, InvoiceExceptionStatus>();
|
||||
_StringToExceptionStatus.Add(string.Empty, InvoiceExceptionStatus.None);
|
||||
_StringToExceptionStatus.Add("paidPartial", InvoiceExceptionStatus.PaidPartial);
|
||||
_StringToExceptionStatus.Add("paidLate", InvoiceExceptionStatus.PaidLate);
|
||||
_StringToExceptionStatus.Add("paidOver", InvoiceExceptionStatus.PaidOver);
|
||||
_StringToExceptionStatus.Add("marked", InvoiceExceptionStatus.Marked);
|
||||
_ExceptionStatusToString = _StringToExceptionStatus.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
_StringToExceptionStatus.Add("false", InvoiceExceptionStatus.None);
|
||||
}
|
||||
public InvoiceState(string status, string exceptionStatus)
|
||||
{
|
||||
Status = _StringToInvoiceStatus[status];
|
||||
ExceptionStatus = _StringToExceptionStatus[exceptionStatus ?? string.Empty];
|
||||
}
|
||||
public InvoiceState(InvoiceStatus status, InvoiceExceptionStatus exceptionStatus)
|
||||
{
|
||||
Status = status;
|
||||
ExceptionStatus = exceptionStatus;
|
||||
}
|
||||
|
||||
public InvoiceStatus Status { get; }
|
||||
public InvoiceExceptionStatus ExceptionStatus { get; }
|
||||
|
||||
public static string ToString(InvoiceStatus status)
|
||||
{
|
||||
return _InvoiceStatusToString[status];
|
||||
}
|
||||
|
||||
public static string ToString(InvoiceExceptionStatus exceptionStatus)
|
||||
{
|
||||
return _ExceptionStatusToString[exceptionStatus];
|
||||
}
|
||||
|
||||
public bool CanMarkComplete()
|
||||
{
|
||||
return (Status == InvoiceStatus.Paid) ||
|
||||
#pragma warning disable CA1305 // Specify IFormatProvider
|
||||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
|
||||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
|
||||
(Status != InvoiceStatus.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked) ||
|
||||
(Status == InvoiceStatus.Invalid);
|
||||
#pragma warning restore CA1305 // Specify IFormatProvider
|
||||
}
|
||||
|
||||
public bool CanMarkInvalid()
|
||||
{
|
||||
return (Status == InvoiceStatus.Paid) ||
|
||||
(Status == InvoiceStatus.New) ||
|
||||
#pragma warning disable CA1305 // Specify IFormatProvider
|
||||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
|
||||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
|
||||
(Status != InvoiceStatus.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
|
||||
#pragma warning restore CA1305 // Specify IFormatProvider;
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(Status) + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentMethodAccounting
|
||||
|
@ -126,7 +126,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
Created = invoice.InvoiceTime,
|
||||
Blob = ToBytes(invoice, null),
|
||||
OrderId = invoice.OrderId,
|
||||
Status = invoice.Status,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
ItemCode = invoice.ProductInformation.ItemCode,
|
||||
CustomerEmail = invoice.RefundMail
|
||||
});
|
||||
@ -316,15 +318,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
|
||||
public async Task UpdateInvoiceStatus(string invoiceId, InvoiceState invoiceState)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
invoiceData.Status = status;
|
||||
invoiceData.ExceptionStatus = exceptionStatus;
|
||||
invoiceData.Status = InvoiceState.ToString(invoiceState.Status);
|
||||
invoiceData.ExceptionStatus = InvoiceState.ToString(invoiceState.ExceptionStatus);
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@ -334,13 +336,26 @@ namespace BTCPayServer.Services.Invoices
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData?.Status != "paid")
|
||||
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkInvalid())
|
||||
return;
|
||||
invoiceData.Status = "invalid";
|
||||
invoiceData.ExceptionStatus = "marked";
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool inludeAddressData = false)
|
||||
public async Task UpdatePaidInvoiceToComplete(string invoiceId)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null || !invoiceData.GetInvoiceState().CanMarkComplete())
|
||||
return;
|
||||
invoiceData.Status = "complete";
|
||||
invoiceData.ExceptionStatus = "marked";
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
public async Task<InvoiceEntity> GetInvoice(string id, bool inludeAddressData = false)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
@ -353,9 +368,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
|
||||
query = query.Where(i => i.Id == id);
|
||||
|
||||
if (storeId != null)
|
||||
query = query.Where(i => i.StoreDataId == storeId);
|
||||
|
||||
var invoice = await query.FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
@ -375,8 +387,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
return paymentEntity;
|
||||
}).ToList();
|
||||
#pragma warning restore CS0618
|
||||
entity.ExceptionStatus = invoice.ExceptionStatus;
|
||||
entity.Status = invoice.Status;
|
||||
var state = invoice.GetInvoiceState();
|
||||
entity.ExceptionStatus = state.ExceptionStatus;
|
||||
entity.Status = state.Status;
|
||||
entity.RefundMail = invoice.CustomerEmail;
|
||||
entity.Refundable = invoice.RefundAddresses.Count != 0;
|
||||
if (invoice.HistoricalAddressInvoices != null)
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Services
|
||||
|
||||
public class LightningConfigurations
|
||||
{
|
||||
public List<LightningConfiguration> Configurations { get; set; } = new List<LightningConfiguration>();
|
||||
public List<object> Configurations { get; set; } = new List<object>();
|
||||
}
|
||||
public class LightningConfiguration
|
||||
{
|
||||
@ -51,5 +51,16 @@ namespace BTCPayServer.Services
|
||||
public bool SSL { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string Macaroon { get; set; }
|
||||
public string RestrictedMacaroon { get; set; }
|
||||
}
|
||||
public class LNDRestConfiguration
|
||||
{
|
||||
public string ChainType { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string Macaroon { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string RestrictedMacaroon { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,10 @@
|
||||
<th>Id</th>
|
||||
<td>@Model.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@Model.State</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created date</th>
|
||||
<td>@Model.CreatedDate.ToBrowserDate()</td>
|
||||
@ -70,14 +74,6 @@
|
||||
<th>Transaction speed</th>
|
||||
<td>@Model.TransactionSpeed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>@Model.Status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status Exception</th>
|
||||
<td>@Model.StatusException</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Refund email</th>
|
||||
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
|
||||
@ -221,7 +217,7 @@
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
@if (Model.StatusException == "paidOver")
|
||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
<th class="text-right">Overpaid</th>
|
||||
}
|
||||
@ -236,7 +232,7 @@
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
@if (Model.StatusException == "paidOver")
|
||||
@if (Model.StatusException == InvoiceExceptionStatus.PaidOver)
|
||||
{
|
||||
<td class="text-right">@payment.Overpaid</td>
|
||||
}
|
||||
|
@ -99,23 +99,7 @@
|
||||
}
|
||||
</td>
|
||||
<td>@invoice.InvoiceId</td>
|
||||
@if (invoice.Status == "paid")
|
||||
{
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a class="dropdown-toggle dropdown-toggle-split" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@invoice.Status <span class="sr-only">Toggle Dropdown</span>
|
||||
</a>
|
||||
<div class="dropdown-menu pull-right">
|
||||
<button class="dropdown-item small" data-toggle="modal" data-target="#myModal" onclick="$('#invoiceId').val('@invoice.InvoiceId')">Make Invalid</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td>@invoice.Status</td>
|
||||
}
|
||||
<td>@invoice.Status</td>
|
||||
<td style="text-align:right">@invoice.AmountCurrency</td>
|
||||
<td style="text-align:right">
|
||||
@if (invoice.ShowCheckout)
|
||||
@ -123,9 +107,32 @@
|
||||
<span>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
|
||||
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
|
||||
-
|
||||
@if (!invoice.CanMarkStatus)
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (invoice.CanMarkStatus)
|
||||
{
|
||||
<a class="dropdown-toggle dropdown-toggle-split" href="#" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Change status <span class="sr-only">Toggle Dropdown</span>
|
||||
</a>
|
||||
<div class="dropdown-menu pull-right">
|
||||
@if (invoice.CanMarkInvalid)
|
||||
{
|
||||
<form method="get" asp-action="ChangeInvoiceState" asp-route-invoiceId="@invoice.InvoiceId" asp-route-newState="invalid">
|
||||
<button class="dropdown-item small">Mark as invalid <span class="fa fa-times"></span></button>
|
||||
</form>
|
||||
}
|
||||
@if (invoice.CanMarkComplete)
|
||||
{
|
||||
<form method="get" asp-action="ChangeInvoiceState" asp-route-invoiceId="@invoice.InvoiceId" asp-route-newState="complete">
|
||||
<button class="dropdown-item small">Mark as complete <span class="fa fa-check-circle"></span></button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -153,28 +160,3 @@
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="myModal" class="modal fade" role="dialog">
|
||||
<form method="post" action="/invoices/invalidatepaid">
|
||||
<input id="invoiceId" name="invoiceId" type="hidden" />
|
||||
<div class="modal-dialog">
|
||||
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Set Invoice status to Invalid</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to invalidate this transaction? This action is NOT undoable!</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
1
BTCPayServer/Views/Invoice/_ViewImports.cshtml
Normal file
1
BTCPayServer/Views/Invoice/_ViewImports.cshtml
Normal file
@ -0,0 +1 @@
|
||||
@using BTCPayServer.Services.Invoices
|
@ -1,41 +0,0 @@
|
||||
@model LndRestServicesViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
|
||||
}
|
||||
|
||||
|
||||
<h4>LND REST</h4>
|
||||
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<p>BTCPay exposes LND Rest services for outside consumption. See connection information below</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="BaseApiUrl">Base API Url</label>
|
||||
<input asp-for="BaseApiUrl" readonly class="form-control" />
|
||||
</div>
|
||||
@if (Model.Macaroon != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Macaroon"></label>
|
||||
<input asp-for="Macaroon" readonly class="form-control" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.CertificateThumbprint != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="CertificateThumbprint"></label>
|
||||
<input asp-for="CertificateThumbprint" readonly class="form-control" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
|
||||
<h4>LND GRPC</h4>
|
||||
<h4>LND @Model.ConnectionType</h4>
|
||||
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -17,14 +17,14 @@
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<p>
|
||||
<span>BTCPay exposes gRPC services for outside consumption, you will find connection information here.<br /></span>
|
||||
<span>BTCPay exposes LND's @Model.ConnectionType service for outside consumption, you will find connection information here.<br /></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<h5>QR Code connection</h5>
|
||||
<p>
|
||||
<span>You can use this QR Code to connect your Zap wallet to your LND instance.<br /></span>
|
||||
<span>You can use this QR Code to connect external software to your LND instance.<br /></span>
|
||||
<span>This QR Code is only valid for 10 minutes</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -61,22 +61,39 @@
|
||||
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
|
||||
</div>
|
||||
<div id="details" class="collapse">
|
||||
<div class="form-group">
|
||||
<label asp-for="Host"></label>
|
||||
<input asp-for="Host" readonly class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SSL"></label>
|
||||
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
|
||||
</div>
|
||||
@if(Model.Macaroon != null)
|
||||
@if (Model.Uri == null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Host"></label>
|
||||
<input asp-for="Host" readonly class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SSL"></label>
|
||||
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Uri"></label>
|
||||
<input asp-for="Uri" readonly class="form-control" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.Macaroon != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Macaroon"></label>
|
||||
<input asp-for="Macaroon" readonly class="form-control" />
|
||||
</div>
|
||||
}
|
||||
@if(Model.CertificateThumbprint != null)
|
||||
@if (Model.RestrictedMacaroon != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="RestrictedMacaroon"></label>
|
||||
<input asp-for="RestrictedMacaroon" readonly class="form-control" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.CertificateThumbprint != null)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="CertificateThumbprint"></label>
|
@ -15,8 +15,9 @@
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-8">
|
||||
<h4>Crypto services</h4>
|
||||
<div class="form-group">
|
||||
<span>You can get access here to LND (gRPC, Rest) or SSH services exposed by your server</span>
|
||||
<span>You can get access here to LND (gRPC, Rest) services exposed by your server</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -37,31 +38,54 @@
|
||||
<td style="text-align:right">
|
||||
@if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC)
|
||||
{
|
||||
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
|
||||
<a asp-action="LNDServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
|
||||
}
|
||||
else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest)
|
||||
{
|
||||
<a asp-action="LndRestServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
|
||||
<a asp-action="LNDServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.HasSSH)
|
||||
{
|
||||
<tr>
|
||||
<td>None</td>
|
||||
<td>SSH</td>
|
||||
<td style="text-align:right">
|
||||
<a asp-action="SSHService">See information</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.ExternalServices.Count != 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4>Other services</h4>
|
||||
<div class="form-group">
|
||||
<span>Other external services</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in Model.ExternalServices)
|
||||
{
|
||||
<tr>
|
||||
<td>@s.Name</td>
|
||||
<td style="text-align:right">
|
||||
<a href="@s.Link" target="_blank">See information</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
||||
|
@ -7,6 +7,24 @@
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
|
||||
<div class="alert alert-warning alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<p>
|
||||
<span>Before you proceed, please understand that Lightning Network is still in the experimental stage. Do not put the money you can't afford to lose. There is a high risk of you losing the money.</span>
|
||||
</p>
|
||||
<p>
|
||||
Take time to familiarize yourself with the risk. There's no backup for LND or c-lightning keys in BTCPay. Your keys are in a hot-wallet. This means :
|
||||
</p>
|
||||
<ul>
|
||||
<li>Most of BTCPay Server deployments run on a pruned node, which, while working, is not officially supported by lightning network implementations.</li>
|
||||
<li>If you erase your BTCPay Server virtual machine - you lose all the funds.</li>
|
||||
<li>If your server gets hacked - a hacker can take all of your funds by accessing your keys.</li>
|
||||
<li>If there is a bug in a lightning network implementation - you might lose all the funds.</li>
|
||||
<li>You approve being #reckless and being the sole responsible party for your loss.</li>
|
||||
<li>You approve to keep on your lightning node only what you can afford to lose.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form method="post">
|
||||
@ -54,7 +72,7 @@
|
||||
<label asp-for="ConnectionString"></label>
|
||||
<input id="lightningurl" asp-for="ConnectionString" class="form-control" />
|
||||
<span asp-validation-for="ConnectionString" class="text-danger"></span>
|
||||
@if(Model.InternalLightningNode != null)
|
||||
@if (Model.InternalLightningNode != null)
|
||||
{
|
||||
<p class="form-text text-muted">
|
||||
You can use the internal lightning node by <a href="#" onclick="$('#lightningurl').val('@Model.InternalLightningNode'); return false;">clicking here</a>
|
||||
|
@ -33,7 +33,7 @@
|
||||
"What happened?": "¿Qué sucedió?",
|
||||
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \nPuedes regresar a {{storeName}} si deseas volver a enviar tu pago.",
|
||||
"InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.",
|
||||
"InvoiceExpired_Body_3": "",
|
||||
"InvoiceExpired_Body_3": "Si recibimos el pago despues, procesaremos tu orden o te contactaremos para un reembolso...",
|
||||
"Invoice ID": "ID de la factura",
|
||||
"Order ID": "ID del pedido",
|
||||
"Return to StoreName": "Regresar a {{storeName}}",
|
||||
|
@ -33,7 +33,7 @@
|
||||
"What happened?": "Šta se desilo?",
|
||||
"InvoiceExpired_Body_1": "Ovaj račun je istekao. Račun važi samo {{maxTimeMinutes}} minuta. \nMožete se vratiti na {{storeName}} ukoliko želite da ponovo inicirate plaćanje.",
|
||||
"InvoiceExpired_Body_2": "Ako ste pokušali da pošaljete uplatu, još uvek nije prihvaćena na mreži. Nismo još zaprimili Vašu uplatu.",
|
||||
"InvoiceExpired_Body_3": "",
|
||||
"InvoiceExpired_Body_3": "Ukoliko vašu uplatu primimo kasnije, procesuiraćemo vašu narudžbinu ili ćemo vas kontaktirati za povraćaj novca...",
|
||||
"Invoice ID": "Broj računa",
|
||||
"Order ID": "Broj narudžbine",
|
||||
"Return to StoreName": "Vrati se na {{storeName}}",
|
||||
|
Reference in New Issue
Block a user