Compare commits

...

9 Commits

24 changed files with 881 additions and 40 deletions

View File

@ -46,12 +46,15 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore(string cryptoCode = "BTC")
public StoresController CreateStore(string cryptoCode = null)
{
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync(string cryptoCode = "BTC")
public string CryptoCode { get; set; } = "BTC";
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
{
cryptoCode = cryptoCode ?? CryptoCode;
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
var store = parent.PayTester.GetController<StoresController>(UserId);
@ -65,7 +68,7 @@ namespace BTCPayServer.Tests
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = "BTC",
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
}, "Save");

View File

@ -392,6 +392,65 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.CryptoCode = "LTC";
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
//////////////////////
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
Assert.Null(invoice.BitcoinAddress);
Assert.NotEqual(1.0, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});
}
}
[Fact]
public void CanPayWithTwoCurrencies()
{

View File

@ -37,7 +37,7 @@ services:
- postgres
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.61
image: nicolasdorier/nbxplorer:1.0.0.68
ports:
- "32838:32838"
expose:

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.76</Version>
<Version>1.0.0.81</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />

View File

@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers
{
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository,
ILogger<AccountController> logger)
SettingsRepository settingsRepository)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_logger = Logs.PayServer;
}
[TempData]
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0)
{
_logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
@ -291,7 +289,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
/// <summary>
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId

View File

@ -33,7 +33,8 @@ namespace BTCPayServer.Controllers
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
@ -55,8 +56,10 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
};
foreach (var data in invoice.GetCryptoData(null))

View File

@ -168,12 +168,10 @@ namespace BTCPayServer.Controllers
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
var gettingRate = rateProvider.GetRateAsync("BTC");
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
entity.Rate = await gettingRate;
}
// So users does not crash if they check depositAddress is not set
entity.DepositAddress = cryptoDatas.First().Value.DepositAddress;
#pragma warning restore CS0618
}

View File

@ -33,7 +33,6 @@ namespace BTCPayServer.Controllers
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
private readonly BTCPayWallet _Wallet;
IHostingEnvironment _Env;
StoreRepository _StoreRepository;
@ -47,7 +46,7 @@ namespace BTCPayServer.Controllers
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env)
{
@ -57,7 +56,6 @@ namespace BTCPayServer.Controllers
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Wallet = wallet;
_Env = env;
_StoreRepository = storeRepository;
}

View File

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
@ -132,6 +137,15 @@ namespace BTCPayServer.Data
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.UniqueId
#pragma warning restore CS0618
});
}

View File

@ -32,6 +32,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<InvoiceEventData> Events
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceEventData
{
public string InvoiceDataId
{
get; set;
}
public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp
{
get; set;
}
public string Message { get; set; }
}
}

View File

@ -8,10 +8,19 @@ namespace BTCPayServer.Events
public class InvoiceDataChangedEvent
{
public string InvoiceId { get; set; }
public string Status { get; internal set; }
public string ExceptionStatus { get; internal set; }
public override string ToString()
{
return $"Invoice {InvoiceId} data changed";
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
{
return $"Invoice status is {Status}";
}
else
{
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
}
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceIPNEvent
{
public InvoiceIPNEvent(string invoiceId)
{
InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public string Error { get; set; }
public override string ToString()
{
if (Error == null)
return $"IPN sent for invoice {InvoiceId}";
return $"Error while sending IPN: {Error}";
}
}
}

View File

@ -9,16 +9,20 @@ namespace BTCPayServer.Events
public class InvoicePaymentEvent
{
public InvoicePaymentEvent(string invoiceId)
public InvoicePaymentEvent(string invoiceId, string cryptoCode, string address)
{
InvoiceId = invoiceId;
Address = address;
CryptoCode = cryptoCode;
}
public string Address { get; set; }
public string CryptoCode { get; private set; }
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} received a payment";
return $"{CryptoCode}: Invoice {InvoiceId} received a payment on {Address}";
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceStopWatchedEvent
{
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} is not monitored anymore.";
}
}
}

View File

@ -68,11 +68,23 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id));
await SendNotification(invoice, cts.Token);
return;
}
catch // It fails, it is OK because we try with hangfire after
catch(OperationCanceledException) when(cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
{
Error = "Timeout"
});
}
catch(Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
@ -93,12 +105,26 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id));
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
}
catch (Exception ex)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
{
Error = "Timeout"
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id)
{
Error = ex.Message
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
}
@ -184,10 +210,42 @@ namespace BTCPayServer.HostedServices
{
await Notify(invoice);
}
await SaveEvent(invoice.Id, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceCreatedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoicePaymentEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
return Task.CompletedTask;
}
private Task SaveEvent(string invoiceId, object evt)
{
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();

View File

@ -99,7 +99,7 @@ namespace BTCPayServer.HostedServices
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
updateContext.Events.Add(new InvoiceDataChangedEvent() { Status = invoice.Status, ExceptionStatus = invoice.ExceptionStatus, InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
@ -118,7 +118,7 @@ namespace BTCPayServer.HostedServices
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
_EventAggregator.Publish<InvoiceStopWatchedEvent>(new InvoiceStopWatchedEvent() { InvoiceId = invoice.Id });
break;
}
@ -169,7 +169,7 @@ namespace BTCPayServer.HostedServices
invoice.Payments.Add(payment);
#pragma warning restore CS0618
alreadyAccounted.Add(coin.Coin.Outpoint);
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
context.Events.Add(new InvoicePaymentEvent(invoice.Id, coins.Wallet.Network.CryptoCode, coin.Coin.ScriptPubKey.GetDestinationAddress(coins.Wallet.Network.NBitcoinNetwork).ToString()));
dirtyAddress = true;
}
if (dirtyAddress)

View File

@ -0,0 +1,508 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180114123253_events")]
partial class events
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class events : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InvoiceEvents",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
UniqueId = table.Column<string>(nullable: false),
Message = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
table.ForeignKey(
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceEvents");
}
}
}

View File

@ -81,6 +81,21 @@ namespace BTCPayServer.Migrations
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
@ -407,6 +422,14 @@ namespace BTCPayServer.Migrations
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

View File

@ -112,6 +112,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public string RedirectUrl { get; set; }
public string Fiat
{
get;
@ -124,5 +126,6 @@ namespace BTCPayServer.Models.InvoicingModels
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}
}

View File

@ -299,6 +299,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public bool IsExpired()
{
@ -369,8 +370,8 @@ namespace BTCPayServer.Services.Invoices
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
dto.CryptoInfo.Add(cryptoInfo);
if(!info.IsPhantomBTC)
dto.CryptoInfo.Add(cryptoInfo);
}
Populate(ProductInformation, dto);
@ -410,22 +411,22 @@ namespace BTCPayServer.Services.Invoices
public Dictionary<string, CryptoData> GetCryptoData(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false)
{
Dictionary<string, CryptoData> rates = new Dictionary<string, CryptoData>();
bool btcAdded = false;
var serializer = new Serializer(Dummy);
CryptoData phantom = null;
#pragma warning disable CS0618
// Legacy
if (alwaysIncludeBTC)
{
var btcNetwork = networkProvider?.GetNetwork("BTC");
rates.Add("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork });
btcAdded = true;
phantom = new CryptoData() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork };
rates.Add("BTC", phantom);
}
if (CryptoData != null)
{
foreach (var prop in CryptoData.Properties())
{
if (prop.Name == "BTC" && btcAdded)
continue;
if (prop.Name == "BTC" && phantom != null)
rates.Remove("BTC");
var r = serializer.ToObject<CryptoData>(prop.Value.ToString());
r.CryptoCode = prop.Name;
r.ParentEntity = this;
@ -511,9 +512,12 @@ namespace BTCPayServer.Services.Invoices
[JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; }
[JsonIgnore]
public bool IsPhantomBTC { get; set; }
public CryptoDataAccounting Calculate()
{
var cryptoData = ParentEntity.GetCryptoData(null);
var cryptoData = ParentEntity.GetCryptoData(null, IsPhantomBTC);
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate);
var paid = Money.Zero;
var cryptoPaid = Money.Zero;

View File

@ -173,8 +173,11 @@ namespace BTCPayServer.Services.Invoices
invoiceEntity.SetCryptoData(currencyData);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
context.AddressInvoices.Add(new AddressInvoiceData() {
InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
@ -188,6 +191,21 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task AddInvoiceEvent(string invoiceId, object evt)
{
using (var context = _ContextFactory.CreateContext())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoiceId,
Message = evt.ToString(),
Timestamp = DateTimeOffset.UtcNow,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
await context.SaveChangesAsync();
}
}
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
{
foreach (var address in entity.GetCryptoData(null))
@ -314,6 +332,10 @@ namespace BTCPayServer.Services.Invoices
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetHash() + a.GetCryptoCode()).ToHashSet();
}
if(invoice.Events != null)
{
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
}
return entity;
}
@ -326,8 +348,10 @@ namespace BTCPayServer.Services.Invoices
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(queryObject.IncludeAddresses)
if (queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
@ -536,5 +560,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool IncludeAddresses { get; set; }
public bool IncludeEvents { get; set; }
}
}

View File

@ -77,6 +77,10 @@
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td>@Model.RedirectUrl</td>
</tr>
</table>
</div>
@ -221,5 +225,28 @@
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Events</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>
<td>@evt.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</section>