Compare commits

..

40 Commits

Author SHA1 Message Date
abeb10cc8c version bump 2017-10-23 18:17:22 +09:00
4dca81403b Better handle transition from paid to invalid 2017-10-23 18:07:50 +09:00
6ba6a34df2 Fix the states of invoice to match bitpay 2017-10-23 17:44:04 +09:00
6d14fe9c30 Do not crash if transactionSpeed not set 2017-10-23 17:05:08 +09:00
c0c4637c77 do not ignore transactionSpeed set at the invoice level 2017-10-23 14:51:21 +09:00
752d34f603 do not throw 500 if using a pairing code which does not exists 2017-10-23 14:12:54 +09:00
0009ed0921 Bug fix: orderId was ignored 2017-10-23 01:23:56 +09:00
953de22233 Add video to readme 2017-10-21 20:53:48 +09:00
1477aef39c remove outdated file 2017-10-21 20:34:30 +09:00
2cca8f82e1 Update README.md 2017-10-21 20:31:36 +09:00
9094af025d Update README.md 2017-10-21 20:30:59 +09:00
0ba608bd1e Update README.md 2017-10-21 20:30:49 +09:00
67f5d497d5 helper script for using cli on regtest 2017-10-21 20:20:06 +09:00
3cc614c024 Add command line so no need of bitcoin-cli in dev 2017-10-21 19:56:55 +09:00
7c20bab1c5 Merge branch 'lepipele-master' 2017-10-21 18:08:49 +09:00
a2a3f43fd0 Small cleanup bugfixes
-Default redirection to / if Url is null
-Removing old Javascript notice
-Passing whole model as function now requires
2017-10-20 23:25:27 -05:00
9f17e3e1f8 Temporary importing legacy graphics 2017-10-20 23:15:49 -05:00
c1a2fc22f4 Reverting back to 15 minutes waiting for invoice 2017-10-20 22:42:15 -05:00
f1f19369a3 Automatic conversion to lower Camel Case for JSON 2017-10-20 22:37:01 -05:00
e49f25af09 Returning whole invoice serialized as JSON on $ajax call 2017-10-20 22:24:28 -05:00
bc28571174 Merge remote-tracking branch 'source/master' 2017-10-20 22:06:54 -05:00
2beae1dcd3 Refactoring logic for referencing server model in js script
Will keep removing unnecessary boilerplate "assign variable" code and try to streamline it as much as possible
2017-10-20 22:06:42 -05:00
39a0b1e29e Merge pull request #4 from lepipele/master
Bunch of small fixes
2017-10-21 11:19:37 +09:00
0f603ffb0a Allowing customization of expiry time for easier debugging 2017-10-20 17:32:52 -05:00
8fe5835e09 Adding text overflow protection on td when invoice is displayed
Responsive layout better maintained this way
2017-10-20 17:15:25 -05:00
5fed7a3a0c Linking back to Invoices after expiry 2017-10-20 17:14:43 -05:00
28ea694791 Extracting my tests to separate class, adding test for generating ExtPubKey 2017-10-20 16:49:13 -05:00
45b0991841 Fixing typo in namespace 2017-10-20 14:06:37 -05:00
505d9904af Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer.Tests/UnitTest1.cs
2017-10-20 14:04:40 -05:00
da385f9295 Adding Unit Test for generating test Bitpay checkout page 2017-10-20 14:00:38 -05:00
214de77a41 Remove launch profile from docker 2017-10-19 17:45:07 +09:00
7b2db3755e fix bip urls formatting 2017-10-19 17:06:51 +09:00
44791cc9f3 bump 2017-10-19 16:47:24 +09:00
a14b94c96f Fix copy/paste BTC amount, add redirect store link to invoice 2017-10-19 16:37:07 +09:00
db1cf5c2ce format currency correctly 2017-10-19 16:08:41 +09:00
1a060a6c7b Fix checkout page 2017-10-19 01:33:55 +09:00
ff719fbe2d bump 2017-10-18 18:45:00 +09:00
94e9ab7f67 In server-initiated situation, the server can set the label 2017-10-18 18:44:24 +09:00
06a96e8b77 README, prevent a nullreferenceexception 2017-10-18 10:40:59 +09:00
d43c3dc968 generate 7 digit pairing code, notify parent windows of checkout 2017-10-17 17:04:33 +09:00
49 changed files with 822 additions and 586 deletions

View File

@ -121,3 +121,4 @@ bower_components
output
.vs
**/launchSettings.json

View File

@ -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;

View File

@ -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
```

View File

@ -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;

View File

@ -9,340 +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()
{
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);
[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 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());
[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);
}
}
}
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);
}
}
[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());
[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);
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);
}
}
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).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();
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).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);
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);
}
}
}
}
}

View 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));
}
}
}

View File

@ -0,0 +1 @@
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args

View File

@ -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"

View File

@ -58,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;
@ -161,6 +167,8 @@ namespace BTCPayServer.Authentication
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
{
if(data == null)
return null;
return new PairingCodeEntity()
{
Facade = data.Facade,

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.15</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.11" />
<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" />

View File

@ -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

View File

@ -56,8 +56,16 @@ 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(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})");

View File

@ -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;

View File

@ -10,7 +10,7 @@ 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;

View File

@ -1,7 +1,7 @@
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;
@ -92,6 +92,27 @@ 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")]
@ -104,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)
@ -152,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]

View File

@ -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;
@ -75,8 +75,9 @@ namespace BTCPayServer.Controllers
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
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
{
@ -87,7 +88,9 @@ 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.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;
@ -103,7 +106,7 @@ namespace BTCPayServer.Controllers
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;
@ -116,6 +119,19 @@ 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)

View File

@ -230,7 +230,7 @@ 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;

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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; }
}
}

View File

@ -1,4 +1,4 @@
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations;
using System;
using System.Collections.Generic;

View File

@ -15,7 +15,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
[Required]
public string Label
{
get; set;

View File

@ -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()
{

View File

@ -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
{

View File

@ -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
{

View File

@ -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

View File

@ -27,22 +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 invoiceId = "@Model.InvoiceId";
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>
@ -56,7 +41,7 @@
<noscript>
<center style="padding: 2em">
<h2>Javascript is currently disabled in your browser.</h2>
<h5>Please enable Javascript from &apos;https://bitpay.com/&apos; 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>
@ -128,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">
@ -145,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">
@ -157,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>
@ -180,20 +163,20 @@
<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="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"></div>
</div>
@ -300,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>
@ -316,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>
<!---->
@ -348,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 &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
@ -380,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>
@ -397,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>
@ -418,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>
@ -427,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>
<!---->
@ -438,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>
<!---->
@ -464,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>
@ -489,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>
@ -539,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">
@ -571,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>
@ -606,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>

View File

@ -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>

View File

@ -3,6 +3,8 @@
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>
@ -12,6 +14,10 @@
<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>

View File

@ -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 {

View 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

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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 = "i/" + invoiceId + "/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 = "i/" + invoiceId + "/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();
}
});
}
);

View File

@ -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)

View File

@ -1,2 +0,0 @@
docker-compose -f docker-compose.regtest.yml down
docker-compose -f docker-compose.regtest.yml up --force-recreate --build