Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
abeb10cc8c | |||
4dca81403b | |||
6ba6a34df2 | |||
6d14fe9c30 | |||
c0c4637c77 | |||
752d34f603 | |||
0009ed0921 | |||
953de22233 | |||
1477aef39c | |||
2cca8f82e1 | |||
9094af025d | |||
0ba608bd1e | |||
67f5d497d5 | |||
3cc614c024 | |||
7c20bab1c5 | |||
a2a3f43fd0 | |||
9f17e3e1f8 | |||
c1a2fc22f4 | |||
f1f19369a3 | |||
e49f25af09 | |||
bc28571174 | |||
2beae1dcd3 | |||
39a0b1e29e | |||
0f603ffb0a | |||
8fe5835e09 | |||
5fed7a3a0c | |||
28ea694791 | |||
45b0991841 | |||
505d9904af | |||
da385f9295 | |||
214de77a41 | |||
7b2db3755e | |||
44791cc9f3 | |||
a14b94c96f | |||
db1cf5c2ce | |||
1a060a6c7b | |||
ff719fbe2d | |||
94e9ab7f67 | |||
06a96e8b77 | |||
d43c3dc968 | |||
e3c5efa929 | |||
8db9d93d23 | |||
517bb94b8b | |||
c804f55d82 | |||
c96a25c9b9 | |||
4c57726be7 | |||
8f723d7131 | |||
a7e10c0fb9 | |||
15e73e1cad | |||
a17192ee99 | |||
27200d1fb0 | |||
9ddceae824 | |||
d1961e0938 |
@ -121,3 +121,4 @@ bower_components
|
||||
output
|
||||
|
||||
.vs
|
||||
**/launchSettings.json
|
||||
|
@ -1,6 +1,6 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Tests.Mocks;
|
||||
|
@ -3,6 +3,8 @@
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
```
|
||||
@ -19,14 +21,26 @@ Once you want to stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
If you want to stop, and remove all existing data
|
||||
|
||||
```
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
The Bitcoin RPC server is exposed to the host, for example, you can send 0.23111090 BTC to mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf.
|
||||
## Send commands to bitcoind
|
||||
|
||||
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=43782 -rpcuser=ceiwHEbqWI83 -rpcpassword=DwubwWsoo3 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
@ -1,7 +1,7 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
|
@ -9,319 +9,341 @@ using System.Threading;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Authentication;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
public UnitTest1(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
public class UnitTest1
|
||||
{
|
||||
public UnitTest1(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
entity.TxFee = Money.Coins(0.1m);
|
||||
entity.Rate = 5000;
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
entity.TxFee = Money.Coins(0.1m);
|
||||
entity.Rate = 5000;
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
|
||||
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
|
||||
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
}
|
||||
Assert.Equal(Money.Zero, entity.GetCryptoDue());
|
||||
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
[Fact]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.False(invoice.Refundable);
|
||||
Assert.False(invoice.Refundable);
|
||||
|
||||
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
|
||||
var request = url.GetPaymentRequest();
|
||||
var payment = request.CreatePayment();
|
||||
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);
|
||||
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.Transactions.Add(tx);
|
||||
|
||||
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
|
||||
var ack = payment.SubmitPayment();
|
||||
Assert.NotNull(ack);
|
||||
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
|
||||
var ack = payment.SubmitPayment();
|
||||
Assert.NotNull(ack);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(url.Address);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.True(localInvoice.Refundable);
|
||||
});
|
||||
}
|
||||
}
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(url.Address);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.True(localInvoice.Refundable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUseServerInitiatedPairingCode()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
acc.CreateStore();
|
||||
|
||||
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
|
||||
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "bla",
|
||||
PublicKey = null
|
||||
}).GetAwaiter().GetResult();
|
||||
[Fact]
|
||||
public void CanUseServerInitiatedPairingCode()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
acc.CreateStore();
|
||||
|
||||
var pairingCode = (string)token.RouteValues["pairingCode"];
|
||||
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
|
||||
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "bla",
|
||||
PublicKey = null
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
|
||||
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
|
||||
}
|
||||
}
|
||||
var pairingCode = (string)token.RouteValues["pairingCode"];
|
||||
|
||||
[Fact]
|
||||
public void CanSendIPN()
|
||||
{
|
||||
using(var callbackServer = new CustomServer())
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
NotificationURL = callbackServer.GetUri().AbsoluteUri,
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
Thread.Sleep(5000);
|
||||
tester.SimulateCallback(url.Address);
|
||||
callbackServer.ProcessNextRequest((ctx) =>
|
||||
{
|
||||
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
|
||||
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
|
||||
});
|
||||
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.NotNull(invoice2);
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
|
||||
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
[Fact]
|
||||
public void CanSendIPN()
|
||||
{
|
||||
using (var callbackServer = new CustomServer())
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
NotificationURL = callbackServer.GetUri().AbsoluteUri,
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
Thread.Sleep(5000);
|
||||
tester.SimulateCallback(url.Address);
|
||||
callbackServer.ProcessNextRequest((ctx) =>
|
||||
{
|
||||
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
|
||||
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
|
||||
});
|
||||
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.NotNull(invoice2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
[Fact]
|
||||
public void CantPairTwiceWithSamePubkey()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
var store = acc.CreateStore();
|
||||
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||
var store2 = acc.CreateStore();
|
||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
|
||||
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
Assert.Equal(1, textSearchResult.Length);
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
|
||||
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
|
||||
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
|
||||
|
||||
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
|
||||
var txFee = Money.Zero;
|
||||
var txFee = Money.Zero;
|
||||
|
||||
var rate = user.BitPay.GetRates();
|
||||
var rate = user.BitPay.GetRates();
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
|
||||
Money secondPayment = Money.Zero;
|
||||
Money secondPayment = Money.Zero;
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidPartial", localInvoice.Status);
|
||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
|
||||
secondPayment = localInvoice.BtcDue;
|
||||
});
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("new", localInvoice.Status);
|
||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
|
||||
secondPayment = localInvoice.BtcDue;
|
||||
});
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
|
||||
cashCow.Generate(5); //Now should be complete
|
||||
cashCow.Generate(5); //Now should be complete
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
});
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paidOver", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback(invoiceAddress);
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
cashCow.Generate(1);
|
||||
cashCow.Generate(1);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
while(true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch(XunitException) when(!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
[Fact]
|
||||
public void BitpayCheckout()
|
||||
{
|
||||
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
var url = new Uri("https://test.bitpay.com/");
|
||||
var btcpay = new Bitpay(key, url);
|
||||
var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
{
|
||||
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
ItemDesc = "Hello from the otherside"
|
||||
}, Facade.Merchant);
|
||||
|
||||
// go to invoice.Url
|
||||
Console.WriteLine(invoice.Url);
|
||||
}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
1
BTCPayServer.Tests/docker-bitcoin-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args
|
@ -39,14 +39,13 @@ services:
|
||||
- postgres
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.15.0.1
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: "regtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
|
||||
BITCOIN_RPC_USER: ceiwHEbqWI83
|
||||
BITCOIN_RPC_PASSWORD: DwubwWsoo3
|
||||
BITCOIN_EXTRA_ARGS: "rpcuser=ceiwHEbqWI83\nrpcpassword=DwubwWsoo3\nregtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
|
||||
expose:
|
||||
- "43782"
|
||||
- "39388"
|
||||
|
@ -13,6 +13,14 @@ using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public enum PairingResult
|
||||
{
|
||||
Partial,
|
||||
Complete,
|
||||
ReusedKey,
|
||||
Expired
|
||||
}
|
||||
|
||||
public class TokenRepository
|
||||
{
|
||||
ApplicationDbContextFactory _Factory;
|
||||
@ -50,7 +58,13 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
public async Task<string> CreatePairingCodeAsync()
|
||||
{
|
||||
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
string pairingCodeId = null;
|
||||
while(true)
|
||||
{
|
||||
pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
|
||||
if(pairingCodeId.Length == 7) // woocommerce plugin check for exactly 7 digits
|
||||
break;
|
||||
}
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@ -79,40 +93,45 @@ namespace BTCPayServer.Authentication
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PairWithStoreAsync(string pairingCodeId, string storeId)
|
||||
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return false;
|
||||
return PairingResult.Expired;
|
||||
pairingCode.StoreDataId = storeId;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> PairWithSINAsync(string pairingCodeId, string sin)
|
||||
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return false;
|
||||
return PairingResult.Expired;
|
||||
pairingCode.SIN = sin;
|
||||
await ActivateIfComplete(ctx, pairingCode);
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
|
||||
{
|
||||
ctx.PairingCodes.Remove(pairingCode);
|
||||
|
||||
// Can have concurrency issues... but no harm can be done
|
||||
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
|
||||
if(alreadyUsed)
|
||||
return PairingResult.ReusedKey;
|
||||
await ctx.PairedSINData.AddAsync(new PairedSINData()
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
@ -122,7 +141,9 @@ namespace BTCPayServer.Authentication
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
});
|
||||
return PairingResult.Complete;
|
||||
}
|
||||
return PairingResult.Partial;
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +167,8 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
|
||||
{
|
||||
if(data == null)
|
||||
return null;
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.11</Version>
|
||||
<Version>1.0.0.23</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
@ -22,7 +22,7 @@
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.12" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
@ -46,7 +46,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\img\bitcoin-symbol.svg" />
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
|
@ -13,7 +13,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
|
@ -56,9 +56,19 @@ namespace BTCPayServer.Controllers
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
if(pairingEntity == null)
|
||||
throw new BitpayHttpException(404, "The specified pairingCode is not found");
|
||||
pairingEntity.SIN = sin;
|
||||
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
|
||||
throw new BitpayHttpException(400, "Unknown pairing code");
|
||||
|
||||
if(string.IsNullOrEmpty(pairingEntity.Label) && !string.IsNullOrEmpty(request.Label))
|
||||
{
|
||||
pairingEntity.Label = request.Label;
|
||||
await _TokenRepository.UpdatePairingCode(pairingEntity);
|
||||
}
|
||||
|
||||
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
|
||||
if(result != PairingResult.Complete && result != PairingResult.Partial)
|
||||
throw new BitpayHttpException(400, $"Error while pairing ({result})");
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
@ -10,26 +10,44 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await CreateInvoiceCore(invoice, store);
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
@ -44,7 +62,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
string token,
|
||||
DateTimeOffset? dateStart = null,
|
||||
|
@ -1,8 +1,9 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
@ -81,7 +82,7 @@ namespace BTCPayServer.Controllers
|
||||
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/{m.TransactionId}";
|
||||
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
@ -91,10 +92,32 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
static Dictionary<string, CultureInfo> _CurrencyProviders = new Dictionary<string, CultureInfo>();
|
||||
private IFormatProvider GetCurrencyProvider(string currency)
|
||||
{
|
||||
lock(_CurrencyProviders)
|
||||
{
|
||||
if(_CurrencyProviders.Count == 0)
|
||||
{
|
||||
foreach(var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
|
||||
{
|
||||
try
|
||||
{
|
||||
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
return _CurrencyProviders.TryGet(currency);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
@ -102,37 +125,47 @@ namespace BTCPayServer.Controllers
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
var model = await GetInvoiceModel(invoiceId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BTCAddress = invoice.DepositAddress.ToString(),
|
||||
BTCAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
|
||||
BTCTotalDue = invoice.GetTotalCryptoDue().ToString(),
|
||||
BTCDue = invoice.GetCryptoDue().ToString(),
|
||||
BtcAddress = invoice.DepositAddress.ToString(),
|
||||
BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
|
||||
BtcTotalDue = invoice.GetTotalCryptoDue().ToString(),
|
||||
BtcDue = invoice.GetCryptoDue().ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = invoice.Rate.ToString(),
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
Rate = invoice.Rate.ToString("C", GetCurrencyProvider(invoice.ProductInformation.Currency)),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
TxFees = invoice.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
|
||||
TxCount = invoice.GetTxCount(),
|
||||
BTCPaid = invoice.GetTotalPaid().ToString(),
|
||||
BtcPaid = invoice.GetTotalPaid().ToString(),
|
||||
Status = invoice.Status
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return View(nameof(Checkout), model);
|
||||
return model;
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
@ -150,10 +183,10 @@ namespace BTCPayServer.Controllers
|
||||
[Route("i/{invoiceId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
var model = await GetInvoiceModel(invoiceId);
|
||||
if(model == null)
|
||||
return NotFound();
|
||||
return Content(invoice.Status);
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -244,7 +277,7 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store);
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
|
@ -29,7 +29,7 @@ using BTCPayServer.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Validations;
|
||||
@ -43,7 +43,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
@ -58,7 +57,6 @@ namespace BTCPayServer.Controllers
|
||||
Network network,
|
||||
InvoiceRepository invoiceRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
IRateProvider rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
@ -69,7 +67,6 @@ namespace BTCPayServer.Controllers
|
||||
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
@ -78,8 +75,9 @@ namespace BTCPayServer.Controllers
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
}
|
||||
|
||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
|
||||
{
|
||||
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
@ -90,16 +88,25 @@ namespace BTCPayServer.Controllers
|
||||
if(notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
|
||||
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
|
||||
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if(entity?.BuyerInformation?.BuyerEmail != null)
|
||||
{
|
||||
if(!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = store.SpeedPolicy;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
|
||||
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.PosData = invoice.PosData;
|
||||
@ -112,6 +119,34 @@ namespace BTCPayServer.Controllers
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if(transactionSpeed == null)
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||
if(!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||
policy = defaultPolicy;
|
||||
return policy;
|
||||
}
|
||||
|
||||
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
|
||||
{
|
||||
if(buyer == null)
|
||||
return;
|
||||
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
|
||||
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
|
||||
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
|
||||
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
|
||||
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
|
||||
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
|
||||
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
|
||||
{
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -21,6 +23,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = "CanAccessStore")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class StoresController : Controller
|
||||
{
|
||||
public StoresController(
|
||||
@ -123,7 +126,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
|
||||
{
|
||||
@ -220,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
@ -228,6 +230,17 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
model.Label = model.Label ?? String.Empty;
|
||||
if(storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
|
||||
{
|
||||
storeId = model.StoreId;
|
||||
var userId = GetUserId();
|
||||
if(userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if(store == null)
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
{
|
||||
@ -262,16 +275,29 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public IActionResult CreateToken()
|
||||
public async Task<IActionResult> CreateToken(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if(string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
model.StoreId = storeId;
|
||||
if(storeId == null)
|
||||
{
|
||||
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("{storeId}/Tokens/Delete")]
|
||||
public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
|
||||
{
|
||||
@ -316,18 +342,21 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("api-access-request")]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
if(pairingCode == null)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(selectedStore, GetUserId());
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if(store == null || pairing == null)
|
||||
return NotFound();
|
||||
if(pairingCode != null && await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id))
|
||||
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if(pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
StatusMessage = "Pairing is successfull";
|
||||
if(pairing.SIN == null)
|
||||
if(pairingResult == PairingResult.Partial)
|
||||
StatusMessage = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
@ -336,7 +365,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = "Pairing failed";
|
||||
StatusMessage = $"Pairing failed ({pairingResult})";
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
|
@ -1,5 +1,5 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
@ -1,9 +1,12 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@ -44,5 +47,12 @@ namespace BTCPayServer
|
||||
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
public static HtmlString ToJson(this object o)
|
||||
{
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return new HtmlString(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
BTCPayServer/Filters/XFrameOptionsAttribute.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XFrameOptionsAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public XFrameOptionsAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
|
||||
if(existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("x-frame-options");
|
||||
else
|
||||
context.HttpContext.Response.Headers["x-frame-options"] = Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ using NBXplorer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Fees;
|
||||
@ -69,12 +69,16 @@ namespace BTCPayServer.Hosting
|
||||
object storeId = null;
|
||||
if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
|
||||
context.Succeed(requirement);
|
||||
else
|
||||
else if(storeId != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User));
|
||||
if(store != null)
|
||||
if(requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
|
||||
if(user != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, user);
|
||||
if(store != null)
|
||||
if(requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,6 +145,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<CallbackController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
|
@ -35,6 +35,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -74,7 +75,10 @@ namespace BTCPayServer.Hosting
|
||||
// Big hack, tests fails because Hangfire fail at initializing at the second test run
|
||||
AddHangfireFix(services);
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
});
|
||||
}
|
||||
|
||||
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||
@ -103,6 +107,13 @@ namespace BTCPayServer.Hosting
|
||||
}));
|
||||
|
||||
services.AddHangfire(configuration);
|
||||
services.AddCors(o =>
|
||||
{
|
||||
o.AddPolicy("BitpayAPI", b =>
|
||||
{
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
|
||||
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
|
||||
{
|
||||
@ -131,7 +142,6 @@ namespace BTCPayServer.Hosting
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
|
||||
app.UseMvc(routes =>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
|
@ -5,103 +5,31 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class PaymentModel
|
||||
{
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public class PaymentModel
|
||||
{
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string BtcAddress { get; set; }
|
||||
public string BtcDue { get; set; }
|
||||
public string CustomerEmail { get; set; }
|
||||
public int ExpirationSeconds { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string MerchantRefLink { get; set; }
|
||||
public int MaxTimeSeconds { get; set; }
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string BTCAddress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
// These properties are not used in client side code
|
||||
public string StoreName { get; set; }
|
||||
public string ItemDesc { get; set; }
|
||||
public string TimeLeft { get; set; }
|
||||
public string Rate { get; set; }
|
||||
public string BtcAmount { get; set; }
|
||||
public string TxFees { get; set; }
|
||||
public string InvoiceBitcoinUrl { get; set; }
|
||||
public string BtcTotalDue { get; set; }
|
||||
public int TxCount { get; set; }
|
||||
public string BtcPaid { get; set; }
|
||||
public string StoreEmail { get; set; }
|
||||
|
||||
public string BTCDue
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string CustomerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int ExpirationSeconds
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int MaxTimeSeconds
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string TimeLeft
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string RedirectUrl
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ItemDesc
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Rate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string BTCAmount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string TxFees
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string InvoiceBitcoinUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string BTCTotalDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public int TxCount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string BTCPaid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string OrderId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Validations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Required]
|
||||
|
||||
public string Label
|
||||
{
|
||||
get; set;
|
||||
@ -25,6 +26,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SelectList Stores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class TokenViewModel
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class BuyerInformation
|
||||
{
|
||||
@ -248,6 +248,11 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public DateTimeOffset? MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
|
@ -16,7 +16,7 @@ using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class InvoiceNotificationManager
|
||||
{
|
||||
|
@ -17,7 +17,7 @@ using BTCPayServer.Data;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class InvoiceRepository
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class InvoiceWatcher : IHostedService
|
||||
{
|
||||
@ -80,7 +80,9 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
|
||||
}
|
||||
|
||||
if(invoice.Status == "complete" || invoice.Status == "invalid")
|
||||
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
|
||||
if(invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
@ -106,110 +108,110 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
|
||||
{
|
||||
bool needSave = false;
|
||||
//Fetch unknown payments
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
if(invoice.Status != "invalid" && invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial"))
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
|
||||
utxos =
|
||||
utxos
|
||||
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
|
||||
.ToArray();
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in utxos)
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
}
|
||||
//////
|
||||
|
||||
if(invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
needSave = true;
|
||||
invoice.Status = "invalid";
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
if(invoice.Status == "invalid" || invoice.Status == "new" || invoice.Status == "paidPartial")
|
||||
if(invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
|
||||
utxos =
|
||||
utxos
|
||||
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
|
||||
.ToArray();
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in utxos)
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
|
||||
if(totalPaid >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
if(invoice.Status == "new")
|
||||
{
|
||||
invoice.Status = "paidPartial";
|
||||
invoice.Status = "paid";
|
||||
if(invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
else if(invoice.Status == "expired")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paidPartial")
|
||||
{
|
||||
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
|
||||
if(totalPaid == invoice.GetTotalCryptoDue())
|
||||
if(totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.Status = "paid";
|
||||
if(invoice.FullNotifications)
|
||||
{
|
||||
_NotificationManager.Notify(invoice);
|
||||
}
|
||||
invoice.ExceptionStatus = null;
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid > invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "paidOver";
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.ExceptionStatus == null)
|
||||
if(totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "paid" || invoice.Status == "paidOver")
|
||||
if(invoice.Status == "paid")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
if(!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => !t.Transaction.Transaction.RBF);
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 1);
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
|
||||
}
|
||||
|
||||
bool confirmed = false;
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
if(minConf > 0)
|
||||
confirmed = true;
|
||||
else
|
||||
confirmed = !transactions.Any(t => t.Transaction.RBF);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
if(totalConfirmed >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
_NotificationManager.Notify(invoice);
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
else
|
||||
{
|
||||
confirmed = minConf >= 1;
|
||||
}
|
||||
else if(invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
confirmed = minConf >= 6;
|
||||
}
|
||||
|
||||
if(confirmed)
|
||||
{
|
||||
invoice.Status = "confirmed";
|
||||
_NotificationManager.Notify(invoice);
|
||||
invoice.Status = "invalid";
|
||||
needSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(invoice.Status == "confirmed")
|
||||
{
|
||||
var getTransactions = invoice.Payments.Select(o => o.Outpoint.Hash).Select(o => _ExplorerClient.GetTransactionAsync(o, _Cts.Token)).ToArray();
|
||||
await Task.WhenAll(getTransactions).ConfigureAwait(false);
|
||||
var transactions = getTransactions.Select(c => c.GetAwaiter().GetResult()).ToArray();
|
||||
var minConf = transactions.Select(t => t.Confirmations).Min();
|
||||
if(minConf >= 6)
|
||||
var transactions = await GetPaymentsWithTransaction(invoice);
|
||||
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
|
||||
if(totalConfirmed >= invoice.GetTotalCryptoDue())
|
||||
{
|
||||
invoice.Status = "complete";
|
||||
if(invoice.FullNotifications)
|
||||
@ -221,6 +223,15 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
return (needSave, changes);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<(PaymentEntity Payment, TransactionResult Transaction)>> GetPaymentsWithTransaction(InvoiceEntity invoice)
|
||||
{
|
||||
var getPayments = invoice.Payments
|
||||
.Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token)))
|
||||
.ToArray();
|
||||
await Task.WhenAll(getPayments).ConfigureAwait(false);
|
||||
var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction));
|
||||
return transactions;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
|
@ -27,21 +27,7 @@
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var btcAddress = "@Model.BTCAddress";
|
||||
var btcDue = "@Model.BTCDue"; //must be a string
|
||||
var customerEmail = "@Model.CustomerEmail"; // Place holder
|
||||
var expirationTime = @Model.ExpirationSeconds; // Can be calculted server-side, or fixed
|
||||
var isArchieved = false; // Preferably Bool
|
||||
var status = "@Model.Status"; // Tx listener
|
||||
var merchantRefLink = "@Model.RedirectUrl"; // Merchant link to redect the user
|
||||
var merchantName = "@Model.StoreName";
|
||||
var merchantDesc = "@Model.ItemDesc";
|
||||
var btcRate = "@Model.Rate";
|
||||
var btcAmount = "@Model.BTCAmount";
|
||||
var itemAmount = 1;
|
||||
var txFees = "@Model.TxFees";
|
||||
var noMock = true;
|
||||
var maxTime = @Model.MaxTimeSeconds;
|
||||
var srvModel = @Model.ToJson();
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
|
||||
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
|
||||
@ -55,7 +41,7 @@
|
||||
<noscript>
|
||||
<center style="padding: 2em">
|
||||
<h2>Javascript is currently disabled in your browser.</h2>
|
||||
<h5>Please enable Javascript from 'https://bitpay.com/' and refresh this page for the best experience.</h5>
|
||||
<h5>Please enable Javascript and refresh this page for the best experience.</h5>
|
||||
|
||||
<p>Alternatively, click below to continue to our HTML-only invoice.</p>
|
||||
|
||||
@ -127,9 +113,7 @@
|
||||
<!---->
|
||||
<div class="single-item-order__right">
|
||||
<div class="single-item-order__right__btc-price clickable" id="buyerTotalBtcAmount">
|
||||
<span>@Model.BTCTotalDue</span>
|
||||
<!---->
|
||||
<img class="single-item-order__right__btc-price__chevron" src="~/img/chevron.svg">
|
||||
<span>@Model.BtcTotalDue</span>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="single-item-order__right__ex-rate">
|
||||
@ -144,7 +128,7 @@
|
||||
<!---->
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label" i18n="">Payment Amount</div>
|
||||
<div class="line-items__item__value">@Model.BTCAmount BTC</div>
|
||||
<div class="line-items__item__value">@Model.BtcAmount BTC</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
@ -156,11 +140,11 @@
|
||||
<div class="line-items__item__label">
|
||||
<span i18n="">Already Paid</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">-@Model.BTCPaid BTC</div>
|
||||
<div class="line-items__item__value" i18n="">-@Model.BtcPaid BTC</div>
|
||||
</div>
|
||||
<div class="line-items__item line-items__item--total">
|
||||
<div class="line-items__item__label" i18n="">Due </div>
|
||||
<div class="line-items__item__value">@Model.BTCDue BTC</div>
|
||||
<div class="line-items__item__value">@Model.BtcDue BTC</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
@ -178,23 +162,23 @@
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan" style="opacity: 1;">
|
||||
<div class="payment__scan">
|
||||
<div class="payment__details__instruction__open-wallet hidden-sm-up">
|
||||
<!---->
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button action-button--secondary">
|
||||
<span i18n="">Show QR code</span>
|
||||
<img class="m-qr-code-icon" src="~/img/qr-code.svg">
|
||||
</a>
|
||||
<div class="m-qr-code-container hidden-sm-up hide">
|
||||
<p class="m-qr-code-header" i18n="">
|
||||
Hide QR code
|
||||
<img class="m-qr-code-expand" src="~/img/chevron.svg">
|
||||
</p>
|
||||
@*<div class="payment__details__instruction__open-wallet hidden-sm-up">
|
||||
<!---->
|
||||
<div class="qr-codes"></div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button action-button--secondary">
|
||||
<span i18n="">Show QR code</span>
|
||||
<img class="m-qr-code-icon" src="~/img/qr-code.svg">
|
||||
</a>
|
||||
<div class="m-qr-code-container hidden-sm-up hide">
|
||||
<p class="m-qr-code-header" i18n="">
|
||||
Hide QR code
|
||||
<img class="m-qr-code-expand" src="~/img/chevron.svg">
|
||||
</p>
|
||||
<!---->
|
||||
<div class="qr-codes"></div>
|
||||
</div>
|
||||
</div>*@
|
||||
<!---->
|
||||
<div class="qr-codes hidden-xs-down"></div>
|
||||
<div class="qr-codes"></div>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" href="@Model.InvoiceBitcoinUrl">
|
||||
@ -299,7 +283,7 @@
|
||||
</div>
|
||||
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
|
||||
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
|
||||
<div><img src="~/img/mail.svg"></div>
|
||||
<div><img src="~/imlegacy/mail.svg"></div>
|
||||
<div class="manual__step-one__header">
|
||||
<span i18n="">Please confirm your address</span>
|
||||
</div>
|
||||
@ -315,7 +299,7 @@
|
||||
</span>
|
||||
<div class="success-text">
|
||||
<!---->
|
||||
<img src="~/img/circle-check.svg">
|
||||
<img src="~/imlegacy/circle-check.svg">
|
||||
<!---->
|
||||
<div i18n="">Email resent</div>
|
||||
<!---->
|
||||
@ -347,7 +331,7 @@
|
||||
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
|
||||
<div class="bp-refund-address">
|
||||
<div class="bitcoin-logo">
|
||||
<div><img src="~/imgs/bitcoin-symbol.svg"></div>
|
||||
<div><img src="~/imlegacy/bitcoin-symbol.svg"></div>
|
||||
</div>
|
||||
<input class="bp-input {'not-empty': addressValue.length > 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length > 0}">
|
||||
</div>
|
||||
@ -379,7 +363,7 @@
|
||||
<div class="manual-box__amount__label label" i18n="">Amount</div>
|
||||
<!---->
|
||||
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
|
||||
<span>@Model.BTCDue BTC</span>
|
||||
<span>@Model.BtcDue</span> BTC
|
||||
<div class="copied-label">
|
||||
<span i18n="">Copied</span>
|
||||
</div>
|
||||
@ -396,9 +380,9 @@
|
||||
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/img/bitcoin-symbol.svg">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">@Model.BTCAddress</div>
|
||||
<div class="manual-box__address__wrapper__value">@Model.BtcAddress</div>
|
||||
</div>
|
||||
<div class="copied-label" style="top: 5px;">
|
||||
<span i18n="">Copied</span>
|
||||
@ -417,7 +401,7 @@
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/img/checkmark.svg">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline"></div>
|
||||
</div>
|
||||
@ -426,6 +410,11 @@
|
||||
<!---->
|
||||
<div class="success-message" i18n="">This invoice has been paid.</div>
|
||||
<!---->
|
||||
<button class="action-button" style="margin-top: 0px;">
|
||||
<bp-done-text>
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to @Model.StoreName</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
@ -437,7 +426,7 @@
|
||||
<div class="bp-view" id="refund-pending">
|
||||
<div class="status-block">
|
||||
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
|
||||
<img src="~/img/refund-pending.svg">
|
||||
<img src="~/imlegacy/refund-pending.svg">
|
||||
<div class="pending-block__header" i18n="">Processing Refund</div>
|
||||
<span>
|
||||
<!---->
|
||||
@ -463,7 +452,7 @@
|
||||
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/img/bitcoin-symbol.svg">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
@ -488,13 +477,13 @@
|
||||
<div class="timeline">
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--complete">
|
||||
<img src="~/img/checkmark-small.svg">
|
||||
<img src="~/imlegacy/checkmark-small.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name" i18n="">Transaction created</div>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--pending">
|
||||
<img src="~/img/pending.svg">
|
||||
<img src="~/imlegacy/pending.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name">
|
||||
<span i18n="">Transaction confirming — funds have not yet moved</span>
|
||||
@ -538,20 +527,20 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-button" style="margin-top: 20px;">
|
||||
<a href="/invoices" class="action-button" style="margin-top: 20px;">
|
||||
<bp-done-text>
|
||||
<!---->
|
||||
<!---->
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to @Model.StoreName</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<div class="bp-view expired" id="archived">
|
||||
<div class="expired-icon">
|
||||
<img src="~/img/archived.svg">
|
||||
<img src="~/imlegacy/archived.svg">
|
||||
</div>
|
||||
<div class="archived__message">
|
||||
<div class="archived__message__header">
|
||||
@ -570,7 +559,7 @@
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/img/checkmark.svg">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
|
||||
</div>
|
||||
@ -605,7 +594,7 @@
|
||||
<div class="manual-box__address__label label" i18n="">Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/img/bitcoin-symbol.svg">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
|
@ -3,6 +3,15 @@
|
||||
ViewData["Title"] = "Invoice " + Model.Id;
|
||||
}
|
||||
|
||||
<style type="text/css">
|
||||
.overflowbox {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
|
||||
@ -86,7 +95,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Url</th>
|
||||
<td><a href="@Model.PaymentUrl">@Model.PaymentUrl</a></td>
|
||||
<td class="overflowbox"><a href="@Model.PaymentUrl">@Model.PaymentUrl</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +16,10 @@
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@await Html.PartialAsync("_Nav")
|
||||
@if(ViewBag.ShowMenu)
|
||||
{
|
||||
@await Html.PartialAsync("_Nav")
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
@RenderBody()
|
||||
|
@ -3,24 +3,33 @@
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Create a new token";
|
||||
ViewData.AddActivePage(StoreNavPages.Tokens);
|
||||
ViewBag.HidePublicKey = ViewBag.HidePublicKey ?? false;
|
||||
ViewBag.ShowStores = ViewBag.ShowStores ?? false;
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form asp-action="CreateToken" method="post">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="Label"></label>
|
||||
@if(ViewBag.HidePublicKey)
|
||||
{
|
||||
<small class="text-muted">optional</small>
|
||||
}
|
||||
<input asp-for="Label" class="form-control" />
|
||||
<span asp-validation-for="Label" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PublicKey"></label>
|
||||
<small class="text-muted">Keep empty for server-initiated pairing</small>
|
||||
<input asp-for="PublicKey" class="form-control" />
|
||||
<span asp-validation-for="PublicKey" class="text-danger"></span>
|
||||
</div>
|
||||
@if(!ViewBag.HidePublicKey)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="PublicKey"></label>
|
||||
<small class="text-muted">Keep empty for server-initiated pairing</small>
|
||||
<input asp-for="PublicKey" class="form-control" />
|
||||
<span asp-validation-for="PublicKey" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Facade"></label>
|
||||
<select asp-for="Facade" class="form-control">
|
||||
@ -29,6 +38,19 @@
|
||||
</select>
|
||||
<span asp-validation-for="Facade" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if(ViewBag.ShowStores)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreId" class="control-label"></label>
|
||||
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
|
||||
<span asp-validation-for="StoreId" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" asp-for="StoreId" />
|
||||
}
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Request pairing" class="btn btn-default" />
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@
|
||||
<td>xpub-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2SH</td>
|
||||
<td>P2PKH</td>
|
||||
<td>xpub-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -8461,7 +8461,6 @@ strong {
|
||||
.single-item-order__right__ex-rate {
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.single-item-order__right__btc-price {
|
||||
|
28
BTCPayServer/wwwroot/imlegacy/archived.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="143px" height="143px" viewBox="0 0 143 143" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7.1 (28215) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group 2</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Invoice-Archived" transform="translate(-340.000000, -179.000000)">
|
||||
<g id="Invoice" transform="translate(236.000000, 87.000000)">
|
||||
<g id="Group-2" transform="translate(105.000000, 93.000000)">
|
||||
<g id="archived">
|
||||
<g id="ios-checkmark-outline" stroke="#9B9B9B">
|
||||
<path d="M70.5,141 C109.436075,141 141,109.436075 141,70.5 C141,31.5639251 109.436075,0 70.5,0 C31.5639251,0 0,31.5639251 0,70.5 C0,109.436075 31.5639251,141 70.5,141 Z" id="Oval-1"></path>
|
||||
</g>
|
||||
<g id="ui-24px-outline-2_archive" opacity="0.6" transform="translate(43.000000, 38.000000)" stroke="#343434">
|
||||
<g id="Group" transform="translate(0.473684, 0.478261)">
|
||||
<rect id="Rectangle-path" x="0" y="23.6521739" width="53.0526316" height="41.3913043"></rect>
|
||||
<path d="M5.89473684,11.826087 L47.1578947,11.826087" id="Shape"></path>
|
||||
<path d="M14.7368421,0 L38.3157895,0" id="Shape"></path>
|
||||
<polyline id="Shape" points="38.3157895 38.4347826 38.3157895 44.3478261 14.7368421 44.3478261 14.7368421 38.4347826"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
14
BTCPayServer/wwwroot/imlegacy/checkmark-small.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="8px" viewBox="0 0 12 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 40.1 (33804) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Line</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Scan---Design-Exploration-(Modal)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
|
||||
<g id="Payment-Success-(Confirming)" transform="translate(-299.000000, -401.000000)" stroke="#FFFFFF">
|
||||
<g id="Group-2" transform="translate(293.000000, 391.000000)">
|
||||
<polyline id="Line" points="7 14.150352 10.0039526 17.1543046 17 10"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 807 B |
16
BTCPayServer/wwwroot/imlegacy/checkmark.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="64px" height="49px" viewBox="0 0 64 49" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7 (28169) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Overpaid-(Email-receipt-sent)" transform="translate(-385.000000, -270.000000)" stroke="#F5F5F7" stroke-width="4" fill="#12E5B6">
|
||||
<g id="Invoice" transform="translate(236.000000, 86.000000)">
|
||||
<g id="ios-checkmark-outline" transform="translate(118.000000, 150.000000)">
|
||||
<path d="M86.5963111,36.9873548 L52.1800773,71.6517825 L39.1636812,58.6353864 L34.2549556,63.5441121 L49.6981374,78.9872939 C50.3875651,79.6767216 51.3251869,80.2282638 52.1525002,80.2282638 C52.9798135,80.2282638 53.8898582,79.6767216 54.5792859,79.014871 L91.4498825,41.9512346 L86.5963111,36.9873548 L86.5963111,36.9873548 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
17
BTCPayServer/wwwroot/imlegacy/circle-check.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="21px" height="21px" viewBox="0 0 21 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7 (28169) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>check</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Overpaid-(Email-receipt-sent)" transform="translate(-384.000000, -628.000000)">
|
||||
<g id="Group-2" transform="translate(384.000000, 628.000000)">
|
||||
<g id="check">
|
||||
<path d="M10.5,21 C16.2989899,21 21,16.2989899 21,10.5 C21,4.70101013 16.2989899,0 10.5,0 C4.70101013,0 0,4.70101013 0,10.5 C0,16.2989899 4.70101013,21 10.5,21 Z" id="Oval-1" fill="#12E5B6"></path>
|
||||
<path d="M15.5429276,6.63875598 L9.3656549,12.8605763 L7.02937868,10.5243001 L6.14832536,11.4053534 L8.92017851,14.1772066 C9.04392195,14.30095 9.21221303,14.3999448 9.36070517,14.3999448 C9.5091973,14.3999448 9.67253865,14.30095 9.79628209,14.1821563 L16.4140815,7.52970878 L15.5429276,6.63875598 L15.5429276,6.63875598 Z" id="Shape" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
21
BTCPayServer/wwwroot/imlegacy/mail.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="99px" height="68px" viewBox="0 0 99 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>586581DD-76A4-41B9-B37A-F862632CDCE3</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Check-email" transform="translate(-675.000000, -223.000000)">
|
||||
<g id="Group-2" transform="translate(0.000000, -32.000000)">
|
||||
<g id="Group-5" transform="translate(527.000000, 196.000000)">
|
||||
<g id="Group-3" transform="translate(149.000000, 59.000000)">
|
||||
<path d="M89.6850586,7.62011719 L3.31494141,7.62011719 C1.49853516,7.62011719 0,9.07324219 0,10.9350586 L0,64.0649414 C0,65.9267578 1.49853516,67.3798828 3.31494141,67.3798828 L89.6850586,67.3798828 C91.5014648,67.3798828 93,65.9267578 93,64.0649414 L93,10.9350586 C93,9.07324219 91.5014648,7.62011719 89.6850586,7.62011719 L89.6850586,7.62011719 Z M86.0522461,10.9350586 L48.8613281,48.1259766 C48.2255859,48.7617188 47.362793,49.125 46.5,49.125 C45.637207,49.125 44.7744141,48.7617188 44.0932617,48.1259766 L6.90234375,10.9350586 L86.0522461,10.9350586 Z M89.6850586,63.065918 L64.1191406,37.5 L89.6850586,11.934082 L89.6850586,63.065918 Z M3.31494141,11.9794922 L28.8354492,37.5 L3.31494141,63.0205078 L3.31494141,11.9794922 Z M6.90234375,64.0649414 L31.1513672,39.7705078 L41.8227539,50.4873047 C43.1396484,51.7587891 44.8198242,52.3945312 46.4545898,52.3945312 C48.1347656,52.3945312 49.8149414,51.7587891 51.0864258,50.4873047 L61.8032227,39.7705078 L86.0522461,64.0649414 L6.90234375,64.0649414 Z" id="mail" stroke="#FFFFFF" fill="#4B62CA"></path>
|
||||
<circle id="Oval-36" fill="#647CE8" cx="88" cy="10" r="10"></circle>
|
||||
<text id="1" font-family="ProximaNova-Regular, Proxima Nova" font-size="11" font-weight="normal" fill="#FFFFFF">
|
||||
<tspan x="85.6355" y="14">1</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
23
BTCPayServer/wwwroot/imlegacy/pending.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="13px" height="13px" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 40.1 (33804) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>arrows-24px-outline-1_refresh-69</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Scan---Design-Exploration-(Modal)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Payment-Success-(Confirming)" transform="translate(-299.000000, -454.000000)" stroke="#D0D0D0">
|
||||
<g id="Group-2" transform="translate(293.000000, 391.000000)">
|
||||
<g id="Group-3" transform="translate(0.000000, 57.000000)">
|
||||
<g id="arrows-24px-outline-1_refresh-69" transform="translate(7.000000, 7.000000)">
|
||||
<g id="Group">
|
||||
<path d="M0,5.5 C0,2.6125 2.475,0.25 5.5,0.25 C7.645,0.25 9.515,1.405 10.395,3.1375" id="Shape"></path>
|
||||
<path d="M11,5.5 C11,8.3875 8.525,10.75 5.5,10.75 C3.355,10.75 1.485,9.595 0.605,7.8625" id="Shape"></path>
|
||||
<polyline id="Shape" stroke-linecap="square" points="10.89 0.0925 10.45 3.19 7.15 2.77"></polyline>
|
||||
<polyline id="Shape" stroke-linecap="square" points="0.11 10.9075 0.55 7.81 3.85 8.23"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
19
BTCPayServer/wwwroot/imlegacy/refund-pending.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="110px" height="110px" viewBox="0 0 110 110" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>bitcoin-topup</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Overpaid---Immediate--Copy-2" transform="translate(-352.000000, -236.000000)">
|
||||
<g id="bitcoin-topup" transform="translate(353.000000, 237.000000)">
|
||||
<g id="Group" transform="translate(0.341463, 0.341463)" stroke="#50E3C2" stroke-width="2">
|
||||
<path d="M107.317073,53.6585366 C107.317073,83.1707317 83.1707317,107.317073 53.6585366,107.317073 C24.1463415,107.317073 0,83.1707317 0,53.6585366 C0,24.1463415 24.1463415,0 53.6585366,0 C74.8536585,0 93.3658537,12.3414634 101.95122,30.3170732" id="Shape"></path>
|
||||
<polyline id="Shape" points="104.634146 10.7317073 101.95122 30.0487805 85.8536585 27.902439"></polyline>
|
||||
</g>
|
||||
<g id="top-up" transform="translate(20.463415, 17.489966)"></g>
|
||||
<path d="M51.906321,29.2490103 C51.906321,28.6339821 52.4042009,28.1653893 52.9899419,28.1653893 C53.575683,28.1653893 54.0735629,28.6339821 54.0735629,29.2490103 L54.0735629,79.5641667 L51.906321,79.5641667 L51.906321,29.2490103 Z M41.8022878,54.9337554 L25.6065477,54.9337554 L33.7190613,33.3491976 L41.8022878,54.9337554 Z M67.9849129,83.8693635 C67.9849129,81.4971122 66.0812545,79.5641667 63.7090032,79.5641667 L56.2115177,79.5641667 L56.2115177,38.8258765 C62.5375211,35.1649949 64.9683465,34.7549762 71.0307664,38.0644131 L61.6296225,63.1341302 C61.6003354,63.2512784 61.5710484,63.3684266 61.5710484,63.5148619 C61.5710484,63.6612971 61.6296225,63.8077324 61.6881966,63.9541677 C62.3325117,67.9372068 66.8134308,71.0123473 72.2608226,71.0123473 C77.7375014,71.0123473 82.2184205,67.9372068 82.8627356,63.9541677 C82.9505968,63.8077324 82.9798839,63.6612971 82.9798839,63.5148619 C82.9798839,63.3684266 82.9505968,63.2512784 82.8920227,63.1341302 L82.9213097,63.1341302 L73.8130364,38.8258765 L73.3151565,36.7464957 C65.1733558,31.8262709 61.6589095,33.2320494 56.2115177,36.2193288 L56.2115177,29.2490103 C56.2115177,27.4625 54.7764521,26.0274345 52.9899419,26.0274345 C51.379154,26.0274345 50.0612367,27.2282036 49.8269402,28.7511304 C44.3502614,25.9981474 41.333695,24.0652019 32.6354403,29.2490103 L32.2547086,31.1526687 L23.0585741,55.6366447 L23.0878612,55.6366447 C23.0292871,55.7537929 23,55.8709411 23,56.0173764 C23,56.1638116 23.0292871,56.3102469 23.1171482,56.4566822 C23.7614634,60.4397213 28.2423824,63.5148619 33.7190613,63.5148619 C39.1664531,63.5148619 43.6766592,60.4397213 44.3209743,56.4566822 C44.3795484,56.3102469 44.4088355,56.1638116 44.4088355,56.0173764 C44.4088355,55.8709411 44.3795484,55.7537929 44.3502614,55.6366447 L34.9491175,30.5376406 C42.0072972,26.6717496 44.115965,28.4582598 49.7683661,31.2991039 L49.7683661,79.5641667 L42.2708807,79.5641667 C39.8986294,79.5641667 37.994971,81.4971122 37.994971,83.8693635 L37.994971,86.0073183 L67.9849129,86.0073183 L67.9849129,83.8693635 Z M72.2608226,40.8466831 L80.3733362,62.4312409 L64.177596,62.4312409 L72.2608226,40.8466831 Z M33.7190613,61.376907 C29.7945962,61.376907 26.4851593,59.5611098 25.4601124,57.0717103 L41.9487231,57.0717103 C40.9236762,59.5611098 37.6435263,61.376907 33.7190613,61.376907 L33.7190613,61.376907 Z M72.2608226,68.8743925 C68.3363575,68.8743925 65.0562076,67.0585952 64.0311608,64.5691958 L80.5197714,64.5691958 C79.4947246,67.0585952 76.1852876,68.8743925 72.2608226,68.8743925 L72.2608226,68.8743925 Z M63.7090032,81.7314086 C64.8804853,81.7314086 65.846958,82.6685943 65.846958,83.8693635 L40.1329258,83.8693635 C40.1329258,82.6685943 41.0993985,81.7314086 42.2708807,81.7314086 L63.7090032,81.7314086 Z" id="scales" fill="#D3D3D3"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -33,12 +33,12 @@
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
// check if the Document expired
|
||||
if (expirationTime > 0) {
|
||||
if (srvModel.expirationSeconds > 0) {
|
||||
|
||||
progressStart(maxTime); // Progress bar
|
||||
startTimer(expirationTime, display); // Timer
|
||||
progressStart(srvModel.maxTimeSeconds); // Progress bar
|
||||
startTimer(srvModel.expirationSeconds, display); // Timer
|
||||
|
||||
if (!validateEmail(customerEmail))
|
||||
if (!validateEmail(srvModel.customerEmail))
|
||||
emailForm(); // Email form Display
|
||||
else
|
||||
hideEmailForm();
|
||||
@ -48,9 +48,9 @@ if (expirationTime > 0) {
|
||||
function hideEmailForm() {
|
||||
$("[role=document]").removeClass("enter-purchaser-email");
|
||||
$("#emailAddressView").removeClass("active");
|
||||
$("placeholder-refundEmail").html(customerEmail);
|
||||
$("placeholder-refundEmail").html(srvModel.customerEmail);
|
||||
// to generate a QR-Code : $(<selector>).qrcode("1Dut19quHiJrXEwfmig4hB8RyLss5aTRTC");
|
||||
$('.qr-codes').qrcode(btcAddress);
|
||||
$('.qr-codes').qrcode(srvModel.btcAddress);
|
||||
|
||||
// Remove Email mode
|
||||
$(".modal-dialog").removeClass("enter-purchaser-email");
|
||||
@ -67,14 +67,14 @@ function emailForm() {
|
||||
if (validateEmail(emailAddress)) {
|
||||
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
|
||||
// Push the email to a server, once the reception is confirmed move on
|
||||
customerEmail = emailAddress;
|
||||
srvModel.customerEmail = emailAddress;
|
||||
|
||||
var path = window.location.pathname + "/UpdateCustomer";
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "POST",
|
||||
data: JSON.stringify({ Email: customerEmail }),
|
||||
data: JSON.stringify({ Email: srvModel.customerEmail }),
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function () {
|
||||
hideEmailForm();
|
||||
@ -94,7 +94,7 @@ function emailForm() {
|
||||
}
|
||||
|
||||
// Copy Tab Info
|
||||
$("#copy .manual__step-two__instructions span").html("To complete your payment, please send " + btcDue + " BTC to the address below.");
|
||||
$("#copy .manual__step-two__instructions span").html("To complete your payment, please send " + srvModel.btcDue + " BTC to the address below.");
|
||||
|
||||
|
||||
/* =============== Even listeners =============== */
|
||||
@ -155,16 +155,32 @@ $("#copy-tab").click(function () {
|
||||
// Should connect using webhook ?
|
||||
// If notification received
|
||||
|
||||
updateState(status);
|
||||
var oldStat = srvModel.status;
|
||||
onDataCallback(srvModel);
|
||||
|
||||
function updateState(status) {
|
||||
if (status == "complete" ||
|
||||
status == "paidOver" ||
|
||||
status == "confirmed" ||
|
||||
status =="paid") {
|
||||
function onDataCallback(jsonData) {
|
||||
var newStatus = jsonData.status;
|
||||
|
||||
if (oldStat != newStatus) {
|
||||
oldStat = newStatus;
|
||||
window.parent.postMessage({ "invoiceId": srvModel.invoiceId, "status": newStatus }, "*");
|
||||
}
|
||||
if (newStatus == "complete" ||
|
||||
newStatus == "confirmed" ||
|
||||
newStatus == "paid") {
|
||||
if ($(".modal-dialog").hasClass("expired")) {
|
||||
$(".modal-dialog").removeClass("expired");
|
||||
}
|
||||
|
||||
if (srvModel.merchantRefLink != "") {
|
||||
$(".action-button").click(function () {
|
||||
window.location.href = srvModel.merchantRefLink;
|
||||
});
|
||||
}
|
||||
else {
|
||||
$(".action-button").hide();
|
||||
}
|
||||
|
||||
$(".modal-dialog").addClass("paid");
|
||||
|
||||
if ($("#scan").hasClass("active")) {
|
||||
@ -175,7 +191,7 @@ function updateState(status) {
|
||||
$("#paid").addClass("active");
|
||||
}
|
||||
|
||||
if (status == "invalid") {
|
||||
if (newStatus == "expired" || newStatus == "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
|
||||
$(".timer-row").removeClass("expiring-soon");
|
||||
$(".timer-row__message span").html("Invoice expired.");
|
||||
$(".timer-row__spinner").html("");
|
||||
@ -186,17 +202,15 @@ function updateState(status) {
|
||||
}
|
||||
|
||||
var watcher = setInterval(function () {
|
||||
var path = window.location.pathname + "/status";
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status";
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "GET"
|
||||
}).done(function (data) {
|
||||
status = data;
|
||||
updateState(status);
|
||||
})
|
||||
.fail(function (jqXHR, textStatus, errorThrown) {
|
||||
onDataCallback(data);
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
|
||||
});
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
$(".menu__item").click(function () {
|
||||
@ -207,11 +221,6 @@ $(".menu__item").click(function () {
|
||||
// function to load contents in different language should go there
|
||||
});
|
||||
|
||||
// Redirect
|
||||
$("#expired .action-button").click(function () {
|
||||
window.location.href = merchantRefLink;
|
||||
});
|
||||
|
||||
// Validate Email address
|
||||
function validateEmail(email) {
|
||||
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
@ -243,11 +252,11 @@ function startTimer(duration, display) {
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
function progressStart(maxTime) {
|
||||
function progressStart(timerMax) {
|
||||
var end = new Date(); // Setup Time Variable, should come from server
|
||||
end.setSeconds(end.getSeconds() + expirationTime);
|
||||
maxTime *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
|
||||
var timeoutVal = Math.floor(maxTime / 100); // Timeout calc
|
||||
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
|
||||
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
|
||||
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
|
||||
animateUpdate(); //Launch it
|
||||
|
||||
function updateProgress(percentage) {
|
||||
@ -258,7 +267,7 @@ function progressStart(maxTime) {
|
||||
|
||||
var now = new Date();
|
||||
var timeDiff = end.getTime() - now.getTime();
|
||||
var perc = 100 - Math.round((timeDiff / maxTime) * 100);
|
||||
var perc = 100 - Math.round((timeDiff / timerMax) * 100);
|
||||
|
||||
if (perc === 75 && (status == "paidPartial" || status == "new")) {
|
||||
$(".timer-row").addClass("expiring-soon");
|
||||
@ -270,7 +279,7 @@ function progressStart(maxTime) {
|
||||
setTimeout(animateUpdate, timeoutVal);
|
||||
}
|
||||
if (perc >= 100 && status == "expired") {
|
||||
updateState(status);
|
||||
onDataCallback(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,4 +310,5 @@ $(document).keypress(
|
||||
if (event.which === '13') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -14,3 +14,9 @@ If you:
|
||||
* Want features BitPay is not willing to consider (Multi-sig + SegWit support soon)
|
||||
|
||||
Then head out to the [documentation](https://github.com/btcpayserver/btcpayserver-doc), this project is for you.
|
||||
|
||||
If you want to help development, go to [Local Development](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Local-Development.md).
|
||||
|
||||
## Additional resources
|
||||
|
||||
[Introduction of BTCPay on youtube](https://www.youtube.com/watch?v=npFMOu6tTpA)
|
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.3
|
||||
VisualStudioVersion = 15.0.26730.16
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServer\BTCPayServer.csproj", "{949A0870-8D8C-4DE5-8845-DDD560489177}"
|
||||
EndProject
|
||||
|
@ -1,2 +0,0 @@
|
||||
docker-compose -f docker-compose.regtest.yml down
|
||||
docker-compose -f docker-compose.regtest.yml up --force-recreate --build
|