mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
backup store
This commit is contained in:
parent
deb9c1e1e1
commit
fcd365b8f6
21 changed files with 3650 additions and 182 deletions
|
@ -34,7 +34,6 @@ public interface IBTCPayAppHubServer
|
|||
Task<bool> BroadcastTransaction(string tx);
|
||||
Task<decimal> GetFeeRate(int blockTarget);
|
||||
Task<BestBlockResponse> GetBestBlock();
|
||||
Task<string> GetBlockHeader(string hash);
|
||||
|
||||
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string[] txIds);
|
||||
Task<string> DeriveScript(string identifier);
|
||||
|
|
32
BTCPayServer.Data/App/AppStorageItemData.cs
Normal file
32
BTCPayServer.Data/App/AppStorageItemData.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using BTCPayServer.Data;
|
||||
using Laraue.EfCoreTriggers.Common.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.App.BackupStorage;
|
||||
|
||||
public class AppStorageItemData
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public long Version { get; set; }
|
||||
public byte[] Value { get; set; }
|
||||
public string UserId { get; set; }
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.HasKey(o => new {o.Key, o.Version, o.UserId});
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.HasIndex(o => new {o.Key, o.UserId}).IsUnique();
|
||||
|
||||
builder.Entity<ApplicationUser>().HasMany(user => user.AppStorageItems).WithOne(data => data.User)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<AppStorageItemData>()
|
||||
.BeforeInsert(trigger => trigger
|
||||
.Action(group => group
|
||||
.Delete<AppStorageItemData>((@ref, entity) => @ref.New.UserId == entity.UserId && @ref.New.Key == entity.Key &&
|
||||
@ref.New.Version > entity.Version)));
|
||||
}
|
||||
|
||||
public ApplicationUser User { get; set; }
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.App.BackupStorage;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
@ -15,6 +17,7 @@ namespace BTCPayServer.Data
|
|||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
// Same as launchsettings.json, it's connecting to the docker's postgres.
|
||||
builder.UseNpgsql("User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=btcpayserver");
|
||||
builder.UsePostgreSqlTriggers();
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +28,8 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
}
|
||||
public DbSet<AddressInvoiceData> AddressInvoices { get; set; }
|
||||
public DbSet<AppStorageItemData> AppStorageItems { get; set; }
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys { get; set; }
|
||||
public DbSet<AppData> Apps { get; set; }
|
||||
public DbSet<StoredFile> Files { get; set; }
|
||||
|
@ -68,7 +73,7 @@ namespace BTCPayServer.Data
|
|||
base.OnModelCreating(builder);
|
||||
|
||||
// some of the data models don't have OnModelCreating for now, commenting them
|
||||
|
||||
AppStorageItemData.OnModelCreating(builder);
|
||||
ApplicationUser.OnModelCreating(builder, Database);
|
||||
AddressInvoiceData.OnModelCreating(builder);
|
||||
APIKeyData.OnModelCreating(builder, Database);
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
|
|||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||
|
@ -20,7 +21,11 @@ namespace BTCPayServer.Data
|
|||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.AddInterceptors(Data.InvoiceData.MigrationInterceptor.Instance);
|
||||
ConfigureBuilder(builder, npgsqlOptionsAction);
|
||||
|
||||
builder.UsePostgreSqlTriggers();
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.PostgreSql" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.App.BackupStorage;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
@ -29,6 +30,9 @@ namespace BTCPayServer.Data
|
|||
|
||||
public List<IdentityUserRole<string>> UserRoles { get; set; }
|
||||
|
||||
public List<AppStorageItemData> AppStorageItems { get; set; }
|
||||
|
||||
|
||||
public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<ApplicationUser>()
|
||||
|
|
1676
BTCPayServer.Data/Migrations/20240724102749_AppStuff.Designer.cs
generated
Normal file
1676
BTCPayServer.Data/Migrations/20240724102749_AppStuff.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
1314
BTCPayServer.Data/Migrations/20240724102749_AppStuff.cs
Normal file
1314
BTCPayServer.Data/Migrations/20240724102749_AppStuff.cs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
|||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -18,11 +18,37 @@ namespace BTCPayServer.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.1")
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<byte[]>("Value")
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<decimal>("Version")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.HasKey("Key", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AppStorageItems", t =>
|
||||
{
|
||||
t.HasTrigger("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA");
|
||||
});
|
||||
|
||||
b.HasAnnotation("LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA", "CREATE FUNCTION \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"() RETURNS trigger as $LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$\r\nBEGIN\r\n DELETE FROM \"AppStorageItems\"\r\n WHERE NEW.\"UserId\" = \"AppStorageItems\".\"UserId\" AND NEW.\"Key\" = \"AppStorageItems\".\"Key\" AND NEW.\"Version\" > \"AppStorageItems\".\"Version\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA BEFORE INSERT\r\nON \"AppStorageItems\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"LC_TRIGGER_BEFORE_INSERT_APPSTORAGEITEMDATA\"();");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
@ -1152,6 +1178,17 @@ namespace BTCPayServer.Migrations
|
|||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.App.BackupStorage.AppStorageItemData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "User")
|
||||
.WithMany("AppStorageItems")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
|
@ -1547,6 +1584,8 @@ namespace BTCPayServer.Migrations
|
|||
{
|
||||
b.Navigation("APIKeys");
|
||||
|
||||
b.Navigation("AppStorageItems");
|
||||
|
||||
b.Navigation("Fido2Credentials");
|
||||
|
||||
b.Navigation("Notifications");
|
||||
|
|
41
BTCPayServer/App/API/ProtobufFormatterAttribute.cs
Normal file
41
BTCPayServer/App/API/ProtobufFormatterAttribute.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufFormatterAttribute : ActionFilterAttribute, IControllerModelConvention, IActionModelConvention
|
||||
{
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
Apply(action);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
// Set the model binder to NewtonsoftJsonBodyModelBinder for parameters that are bound to the request body.
|
||||
var parameters = action.Parameters.Where(p => p.BindingInfo?.BindingSource == BindingSource.Body);
|
||||
foreach (var p in parameters)
|
||||
{
|
||||
p.BindingInfo.BinderType = typeof(ProtobufFormatterModelBinder);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
objectResult.Formatters.Clear();
|
||||
objectResult.Formatters.Add(new ProtobufOutputFormatter());
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnActionExecuted(context);
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/App/API/ProtobufFormatterModelBinder.cs
Normal file
16
BTCPayServer/App/API/ProtobufFormatterModelBinder.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufFormatterModelBinder : BodyModelBinder
|
||||
{
|
||||
private static readonly IInputFormatter[] _inputFormatter = [new ProtobufInputFormatter()];
|
||||
|
||||
public ProtobufFormatterModelBinder(ILoggerFactory loggerFactory, IHttpRequestStreamReaderFactory readerFactory) :
|
||||
base(_inputFormatter, readerFactory, loggerFactory)
|
||||
{
|
||||
}
|
||||
}
|
37
BTCPayServer/App/API/ProtobufInputFormatter.cs
Normal file
37
BTCPayServer/App/API/ProtobufInputFormatter.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufInputFormatter : InputFormatter
|
||||
{
|
||||
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var request = context.HttpContext.Request;
|
||||
if (request.ContentLength == 0)
|
||||
{
|
||||
return await InputFormatterResult.SuccessAsync(null);
|
||||
}
|
||||
|
||||
if (context.HttpContext.Request.ContentType != "application/octet-stream")
|
||||
{
|
||||
return await InputFormatterResult.FailureAsync();
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await request.Body.CopyToAsync(ms);
|
||||
var bytes = ms.ToArray();
|
||||
var messageType = context.ModelType;
|
||||
var message = (IMessage)Activator.CreateInstance(messageType);
|
||||
message.MergeFrom(bytes);
|
||||
return await InputFormatterResult.SuccessAsync(message);
|
||||
}
|
||||
}
|
43
BTCPayServer/App/API/ProtobufOutputFormatter.cs
Normal file
43
BTCPayServer/App/API/ProtobufOutputFormatter.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ProtobufOutputFormatter : OutputFormatter
|
||||
{
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
var responseHeaders = response.Headers;
|
||||
var responseContentType = response.ContentType;
|
||||
if (string.IsNullOrEmpty(responseContentType))
|
||||
{
|
||||
responseContentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
responseHeaders[HeaderNames.ContentType] = responseContentType;
|
||||
|
||||
if (context.Object is IMessage v)
|
||||
{
|
||||
var responseBytes = v.ToByteArray();
|
||||
await response.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||
}
|
||||
else if (context.Object is byte[] bytes)
|
||||
{
|
||||
await response.Body.WriteAsync(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (context.Object is Stream stream)
|
||||
{
|
||||
await stream.CopyToAsync(response.Body);
|
||||
}
|
||||
}
|
||||
}
|
22
BTCPayServer/App/API/ResultOverrideFilter.cs
Normal file
22
BTCPayServer/App/API/ResultOverrideFilter.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
public class ResultOverrideFilter : ResultFilterAttribute
|
||||
{
|
||||
public void OnResultExecuted(ResultExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (context.HttpContext.Items.TryGetValue("Result", out var result) && result is IActionResult value)
|
||||
{
|
||||
context.Result = value;
|
||||
}
|
||||
else if (context.Result is ObjectResult objectResult)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
173
BTCPayServer/App/API/VSSController.cs
Normal file
173
BTCPayServer/App/API/VSSController.cs
Normal file
|
@ -0,0 +1,173 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayApp.VSS;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.App.BackupStorage;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin.Crypto;
|
||||
using VSSProto;
|
||||
|
||||
namespace BTCPayServer.App.API;
|
||||
|
||||
[ApiController]
|
||||
[ResultOverrideFilter]
|
||||
[ProtobufFormatter]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldBearer)]
|
||||
[Route("vss")]
|
||||
public class VSSController : Controller, IVSSAPI
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly BTCPayAppState _appState;
|
||||
|
||||
public VSSController(ApplicationDbContextFactory dbContextFactory,
|
||||
UserManager<ApplicationUser> userManager, BTCPayAppState appState)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_userManager = userManager;
|
||||
_appState = appState;
|
||||
}
|
||||
|
||||
[HttpPost(HttpVSSAPIClient.GET_OBJECT)]
|
||||
[MediaTypeConstraint("application/octet-stream")]
|
||||
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request)
|
||||
{
|
||||
|
||||
var userId = _userManager.GetUserId(User);
|
||||
await using var dbContext = _dbContextFactory.CreateContext();
|
||||
var store = await dbContext.AppStorageItems.SingleOrDefaultAsync(data =>
|
||||
data.Key == request.Key && data.UserId == userId);
|
||||
if (store == null)
|
||||
{
|
||||
return SetResult<GetObjectResponse>(
|
||||
new NotFoundObjectResult(new ErrorResponse()
|
||||
{
|
||||
ErrorCode = ErrorCode.NoSuchKeyException, Message = "Key not found"
|
||||
}));
|
||||
}
|
||||
|
||||
return new GetObjectResponse()
|
||||
{
|
||||
Value = new KeyValue()
|
||||
{
|
||||
Key = store.Key, Value = ByteString.CopyFrom(store.Value), Version = store.Version
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private T SetResult<T>(IActionResult result)
|
||||
{
|
||||
HttpContext.Items["Result"] = result;
|
||||
return default;
|
||||
}
|
||||
|
||||
private bool VerifyGlobalVersion(long globalVersion)
|
||||
{
|
||||
|
||||
var userId = _userManager.GetUserId(User);
|
||||
if (!_appState.GroupToConnectionId.TryGetValues(userId, out var connections))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var node = _appState.NodeToConnectionId.SingleOrDefault(data => connections.Contains(data.Value));
|
||||
if (node.Key == null || node.Value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// This has a high collision rate, but we're not expecting something insane here since we have auth and other checks in place.
|
||||
return globalVersion ==( node.Key + node.Value).GetHashCode();
|
||||
}
|
||||
|
||||
[HttpPost(HttpVSSAPIClient.PUT_OBJECTS)]
|
||||
[MediaTypeConstraint("application/octet-stream")]
|
||||
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request)
|
||||
{
|
||||
|
||||
if (!VerifyGlobalVersion(request.GlobalVersion))
|
||||
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
|
||||
{
|
||||
ErrorCode = ErrorCode.ConflictException, Message = "Global version mismatch"
|
||||
}));
|
||||
|
||||
var userId = _userManager.GetUserId(User);
|
||||
|
||||
await using var dbContext = _dbContextFactory.CreateContext();
|
||||
|
||||
await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
if (request.TransactionItems.Any())
|
||||
{
|
||||
var items = request.TransactionItems.Select(data => new AppStorageItemData()
|
||||
{
|
||||
Key = data.Key, Value = data.Value.ToByteArray(), UserId = userId, Version = data.Version
|
||||
});
|
||||
await dbContext.AppStorageItems.AddRangeAsync(items);
|
||||
}
|
||||
|
||||
if (request.DeleteItems.Any())
|
||||
{
|
||||
var deleteQuery = request.DeleteItems.Aggregate(
|
||||
dbContext.AppStorageItems.Where(data => data.UserId == userId),
|
||||
(current, key) => current.Where(data => data.Key == key.Key && data.Version == key.Version));
|
||||
await deleteQuery.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContextTransaction.CommitAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await dbContextTransaction.RollbackAsync();
|
||||
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse()
|
||||
{
|
||||
ErrorCode = ErrorCode.ConflictException, Message = e.Message
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
return new PutObjectResponse();
|
||||
}
|
||||
|
||||
[HttpPost(HttpVSSAPIClient.DELETE_OBJECT)]
|
||||
[MediaTypeConstraint("application/octet-stream")]
|
||||
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request)
|
||||
{
|
||||
|
||||
|
||||
var userId = _userManager.GetUserId(User);
|
||||
await using var dbContext = _dbContextFactory.CreateContext();
|
||||
var store = await dbContext.AppStorageItems
|
||||
.Where(data => data.Key == request.KeyValue.Key && data.UserId == userId &&
|
||||
data.Version == request.KeyValue.Version).ExecuteDeleteAsync();
|
||||
return store == 0
|
||||
? SetResult<DeleteObjectResponse>(
|
||||
new NotFoundObjectResult(new ErrorResponse()
|
||||
{
|
||||
ErrorCode = ErrorCode.NoSuchKeyException, Message = "Key not found"
|
||||
}))
|
||||
: new DeleteObjectResponse();
|
||||
}
|
||||
|
||||
[HttpPost(HttpVSSAPIClient.LIST_KEY_VERSIONS)]
|
||||
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest request)
|
||||
{
|
||||
var userId = _userManager.GetUserId(User);
|
||||
await using var dbContext = _dbContextFactory.CreateContext();
|
||||
var items = await dbContext.AppStorageItems
|
||||
.Where(data => data.UserId == userId)
|
||||
.Select(data => new KeyValue() {Key = data.Key, Version = data.Version}).ToListAsync();
|
||||
return new ListKeyVersionsResponse {KeyVersions = {items}};
|
||||
}
|
||||
}
|
|
@ -93,14 +93,14 @@ public class BlockHeaders : IEnumerable<RPCBlockHeader>
|
|||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldBearer)]
|
||||
public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
||||
private readonly BTCPayAppState _appState;
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
private readonly ILogger<BTCPayAppHub> _logger;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly ExplorerClient _explorerClient;
|
||||
private readonly BTCPayNetwork _network;
|
||||
|
||||
public BTCPayAppHub(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
NBXplorerDashboard nbXplorerDashboard,
|
||||
|
@ -108,38 +108,32 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
ExplorerClientProvider explorerClientProvider,
|
||||
IFeeProviderFactory feeProviderFactory,
|
||||
ILogger<BTCPayAppHub> logger,
|
||||
StoreRepository storeRepository,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_nbXplorerDashboard = nbXplorerDashboard;
|
||||
_appState = appState;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
_logger = logger;
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
|
||||
_network = btcPayNetworkProvider.BTC;
|
||||
_explorerClient = _explorerClientProvider.GetExplorerClient(btcPayNetworkProvider.BTC);
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
await _appState.Connected(Context.ConnectionId);
|
||||
|
||||
// TODO: this needs to happen BEFORE connection is established
|
||||
if (!_nbXplorerDashboard.IsFullySynched(_btcPayNetworkProvider.BTC.CryptoCode, out _))
|
||||
{
|
||||
Context.Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = _userManager.GetUserId(Context.User!)!;
|
||||
var userStores = await _storeRepository.GetStoresByUserId(userId);
|
||||
await Clients.Client(Context.ConnectionId).NotifyNetwork(_btcPayNetworkProvider.BTC.NBitcoinNetwork.ToString());
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
|
||||
foreach (var userStore in userStores)
|
||||
await _appState.Connected(Context.ConnectionId, userId);
|
||||
|
||||
// TODO: this needs to happen BEFORE connection is established
|
||||
if (!_nbXplorerDashboard.IsFullySynched(_explorerClient.CryptoCode, out _))
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, userStore.Id);
|
||||
Context.Abort();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
|
@ -150,9 +144,8 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
|
||||
public async Task<bool> BroadcastTransaction(string tx)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
Transaction txObj = Transaction.Parse(tx, explorerClient.Network.NBitcoinNetwork);
|
||||
var result = await explorerClient.BroadcastAsync(txObj);
|
||||
Transaction txObj = Transaction.Parse(tx, _network.NBitcoinNetwork);
|
||||
var result = await _explorerClient.BroadcastAsync(txObj);
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
|
@ -160,7 +153,7 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
{
|
||||
_logger.LogInformation($"Getting fee rate for {blockTarget}");
|
||||
|
||||
var feeProvider = _feeProviderFactory.CreateFeeProvider( _btcPayNetworkProvider.BTC);
|
||||
var feeProvider = _feeProviderFactory.CreateFeeProvider( _network);
|
||||
try
|
||||
{
|
||||
return (await feeProvider.GetFeeRateAsync(blockTarget)).SatoshiPerByte;
|
||||
|
@ -174,34 +167,31 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
public async Task<BestBlockResponse> GetBestBlock()
|
||||
{
|
||||
_logger.LogInformation($"Getting best block");
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var bcInfo = await explorerClient.RPCClient.GetBlockchainInfoAsyncEx();
|
||||
var bh = await GetBlockHeader(bcInfo.BestBlockHash.ToString());
|
||||
var bcInfo = await _explorerClient.RPCClient.GetBlockchainInfoAsyncEx();
|
||||
var bh = await GetBlockHeader(bcInfo.BestBlockHash);
|
||||
_logger.LogInformation("Getting best block done");
|
||||
return new BestBlockResponse
|
||||
{
|
||||
BlockHash = bcInfo.BestBlockHash.ToString(),
|
||||
BlockHeight = bcInfo.Blocks,
|
||||
BlockHeader = bh
|
||||
BlockHeader = Convert.ToHexString(bh.ToBytes())
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string> GetBlockHeader(string hash)
|
||||
private async Task<BlockHeader> GetBlockHeader(uint256 hash)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var bh = await explorerClient.RPCClient.GetBlockHeaderAsync(uint256.Parse(hash));
|
||||
return Convert.ToHexString(bh.ToBytes()).ToLower();
|
||||
var bh = await _explorerClient.RPCClient.GetBlockHeaderAsync(hash);
|
||||
return bh;
|
||||
}
|
||||
|
||||
public async Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string[] txIds)
|
||||
{
|
||||
var cancellationToken = Context.ConnectionAborted;
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var uints = txIds.Select(uint256.Parse).ToArray();
|
||||
var txsFetch = await Task.WhenAll(uints.Select(uint256 =>
|
||||
explorerClient.GetTransactionAsync(uint256, cancellationToken)));
|
||||
_explorerClient.GetTransactionAsync(uint256, cancellationToken)));
|
||||
|
||||
var batch = explorerClient.RPCClient.PrepareBatch();
|
||||
var batch = _explorerClient.RPCClient.PrepareBatch();
|
||||
var headersTask = txsFetch.Where(result => result.BlockId is not null && result.BlockId != uint256.Zero)
|
||||
.Distinct().ToDictionary(result => result.BlockId, result =>
|
||||
batch.GetBlockHeaderAsync(result.BlockId, cancellationToken));
|
||||
|
@ -226,34 +216,31 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
public async Task<string> DeriveScript(string identifier)
|
||||
{
|
||||
var cancellationToken = Context.ConnectionAborted;
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var ts = TrackedSource.Parse(identifier,explorerClient.Network ) as DerivationSchemeTrackedSource;
|
||||
var kpi = await explorerClient.GetUnusedAsync(ts.DerivationStrategy, DerivationFeature.Deposit, 0, true, cancellationToken);
|
||||
var ts = TrackedSource.Parse(identifier,_explorerClient.Network ) as DerivationSchemeTrackedSource;
|
||||
var kpi = await _explorerClient.GetUnusedAsync(ts.DerivationStrategy, DerivationFeature.Deposit, 0, true, cancellationToken);
|
||||
return kpi.ScriptPubKey.ToHex();
|
||||
}
|
||||
|
||||
public async Task TrackScripts(string identifier, string[] scripts)
|
||||
{
|
||||
_logger.LogInformation($"Tracking {scripts.Length} scripts for {identifier}");
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
|
||||
var ts = TrackedSource.Parse(identifier,explorerClient.Network ) as GroupTrackedSource;
|
||||
var s = scripts.Select(Script.FromHex).Select(script => script.GetDestinationAddress(explorerClient.Network.NBitcoinNetwork)).Select(address => address.ToString()).ToArray();
|
||||
await explorerClient.AddGroupAddressAsync(explorerClient.CryptoCode,ts.GroupId, s);
|
||||
var ts = TrackedSource.Parse(identifier,_explorerClient.Network ) as GroupTrackedSource;
|
||||
var s = scripts.Select(Script.FromHex).Select(script => script.GetDestinationAddress(_explorerClient.Network.NBitcoinNetwork)).Select(address => address.ToString()).ToArray();
|
||||
await _explorerClient.AddGroupAddressAsync(_explorerClient.CryptoCode,ts.GroupId, s);
|
||||
|
||||
_logger.LogInformation($"Tracking {scripts.Length} scripts for {identifier} done ");
|
||||
}
|
||||
|
||||
public async Task<string> UpdatePsbt(string[] identifiers, string psbt)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var resultPsbt = PSBT.Parse(psbt, explorerClient.Network.NBitcoinNetwork);
|
||||
var resultPsbt = PSBT.Parse(psbt, _explorerClient.Network.NBitcoinNetwork);
|
||||
foreach (string identifier in identifiers)
|
||||
{
|
||||
var ts = TrackedSource.Parse(identifier,explorerClient.Network);
|
||||
var ts = TrackedSource.Parse(identifier,_explorerClient.Network);
|
||||
if (ts is not DerivationSchemeTrackedSource derivationSchemeTrackedSource)
|
||||
continue;
|
||||
var res = await explorerClient.UpdatePSBTAsync(new UpdatePSBTRequest()
|
||||
var res = await _explorerClient.UpdatePSBTAsync(new UpdatePSBTRequest()
|
||||
{
|
||||
PSBT = resultPsbt, DerivationScheme = derivationSchemeTrackedSource.DerivationStrategy,
|
||||
});
|
||||
|
@ -264,23 +251,22 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
|
||||
public async Task<CoinResponse[]> GetUTXOs(string[] identifiers)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var result = new List<CoinResponse>();
|
||||
foreach (string identifier in identifiers)
|
||||
{
|
||||
var ts = TrackedSource.Parse(identifier,explorerClient.Network);
|
||||
var ts = TrackedSource.Parse(identifier,_explorerClient.Network);
|
||||
if (ts is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var utxos = await explorerClient.GetUTXOsAsync(ts);
|
||||
var utxos = await _explorerClient.GetUTXOsAsync(ts);
|
||||
result.AddRange(utxos.GetUnspentUTXOs(0).Select(utxo => new CoinResponse()
|
||||
{
|
||||
Identifier = identifier,
|
||||
Confirmed = utxo.Confirmations >0,
|
||||
Script = utxo.ScriptPubKey.ToHex(),
|
||||
Outpoint = utxo.Outpoint.ToString(),
|
||||
Value = utxo.Value.GetValue(_btcPayNetworkProvider.BTC),
|
||||
Value = utxo.Value.GetValue(_network),
|
||||
Path = utxo.KeyPath?.ToString()
|
||||
}));
|
||||
}
|
||||
|
@ -289,22 +275,21 @@ public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
|||
|
||||
public async Task<Dictionary<string, TxResp[]>> GetTransactions(string[] identifiers)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var result = new Dictionary<string, TxResp[]>();
|
||||
foreach (string identifier in identifiers)
|
||||
{
|
||||
var ts = TrackedSource.Parse(identifier,explorerClient.Network);
|
||||
var ts = TrackedSource.Parse(identifier,_explorerClient.Network);
|
||||
if (ts is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var txs = await explorerClient.GetTransactionsAsync(ts);
|
||||
var txs = await _explorerClient.GetTransactionsAsync(ts);
|
||||
|
||||
var items = txs.ConfirmedTransactions.Transactions
|
||||
.Concat(txs.UnconfirmedTransactions.Transactions)
|
||||
.Concat(txs.ImmatureTransactions.Transactions)
|
||||
.Concat(txs.ReplacedTransactions.Transactions)
|
||||
.Select(tx => new TxResp(tx.Confirmations, tx.Height, tx.BalanceChange.GetValue(_btcPayNetworkProvider.BTC), tx.Timestamp, tx.TransactionId.ToString())).OrderByDescending(arg => arg.Timestamp);
|
||||
.Select(tx => new TxResp(tx.Confirmations, tx.Height, tx.BalanceChange.GetValue(_network), tx.Timestamp, tx.TransactionId.ToString())).OrderByDescending(arg => arg.Timestamp);
|
||||
result.Add(identifier,items.ToArray());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -13,59 +11,18 @@ using NBitcoin;
|
|||
|
||||
namespace BTCPayServer.App;
|
||||
|
||||
public class BTCPayAppLightningConnectionStringHandler:ILightningConnectionStringHandler
|
||||
{
|
||||
private readonly IHubContext<BTCPayAppHub, IBTCPayAppHubClient> _hubContext;
|
||||
private readonly BTCPayAppState _appState;
|
||||
private readonly DefaultHubLifetimeManager<BTCPayAppHub> _lifetimeManager;
|
||||
|
||||
public BTCPayAppLightningConnectionStringHandler(IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext, BTCPayAppState appState)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_appState = appState;
|
||||
}
|
||||
|
||||
public ILightningClient Create(string connectionString, Network network, [UnscopedRef] out string error)
|
||||
{
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type);
|
||||
if (type != "app")
|
||||
{
|
||||
error = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!kv.TryGetValue("group", out var key))
|
||||
{
|
||||
error = $"The key 'group' is mandatory for app connection strings";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!_appState.GroupToConnectionId.TryGetValue(key, out var connectionId))
|
||||
{
|
||||
error = $"The group {key} is not connected";
|
||||
return null;
|
||||
}
|
||||
error = null;
|
||||
return new BTCPayAppLightningClient(_hubContext, _appState, key, network );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class BTCPayAppLightningClient : ILightningClient
|
||||
{
|
||||
private readonly IHubContext<BTCPayAppHub, IBTCPayAppHubClient> _hubContext;
|
||||
private readonly BTCPayAppState _appState;
|
||||
private readonly string _key;
|
||||
private readonly Network _network;
|
||||
|
||||
public BTCPayAppLightningClient(IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext, BTCPayAppState appState, string key, Network network)
|
||||
public BTCPayAppLightningClient(IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext, BTCPayAppState appState,
|
||||
string key)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_appState = appState;
|
||||
_key = key;
|
||||
_network = network;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -73,15 +30,19 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
return $"type=app;group={_key}".ToLower();
|
||||
}
|
||||
|
||||
public IBTCPayAppHubClient HubClient => _appState.GroupToConnectionId.TryGetValue(_key, out var connId) ? _hubContext.Clients.Client(connId) : throw new InvalidOperationException("Connection not found");
|
||||
public IBTCPayAppHubClient HubClient => _appState.NodeToConnectionId.TryGetValue(_key, out var connId)
|
||||
? _hubContext.Clients.Client(connId)
|
||||
: throw new InvalidOperationException("Connection not found");
|
||||
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await GetInvoice(uint256.Parse(invoiceId), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await HubClient.GetLightningInvoice(paymentHash);
|
||||
}
|
||||
|
@ -91,23 +52,26 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
return await ListInvoices(new ListInvoicesParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return (await HubClient.GetLightningInvoices(request)).ToArray();
|
||||
|
||||
}
|
||||
|
||||
public async Task<Lightning.LightningPayment> GetPayment(string paymentHash, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<Lightning.LightningPayment> GetPayment(string paymentHash,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
|
||||
return await HubClient.GetLightningPayment(uint256.Parse(paymentHash));
|
||||
}
|
||||
public async Task<Lightning.LightningPayment[]> ListPayments(CancellationToken cancellation = new CancellationToken())
|
||||
|
||||
public async Task<Lightning.LightningPayment[]> ListPayments(
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await ListPayments(new ListPaymentsParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return (await HubClient.GetLightningPayments(request)).ToArray();
|
||||
}
|
||||
|
@ -118,15 +82,18 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
return await CreateInvoice(new CreateInvoiceParams(amount, description, expiry), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await HubClient.CreateInvoice(new CreateLightningInvoiceRequest(createInvoiceRequest.Amount, createInvoiceRequest.Description, createInvoiceRequest.Expiry)
|
||||
return await HubClient.CreateInvoice(
|
||||
new CreateLightningInvoiceRequest(createInvoiceRequest.Amount, createInvoiceRequest.Description,
|
||||
createInvoiceRequest.Expiry)
|
||||
{
|
||||
DescriptionHashOnly = createInvoiceRequest.DescriptionHashOnly,
|
||||
PrivateRouteHints = createInvoiceRequest.PrivateRouteHints,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return new Listener(_appState, _key);
|
||||
|
@ -158,7 +125,6 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
{
|
||||
if (e.Item1.Equals(_key, StringComparison.InvariantCultureIgnoreCase))
|
||||
_channel.Writer.TryWrite(e.Item2);
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -186,12 +152,14 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(PayInvoiceParams payParams, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<PayResponse> Pay(PayInvoiceParams payParams,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await Pay(null, payParams, cancellation);
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<PayResponse> Pay(string bolt11, PayInvoiceParams payParams,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await HubClient.PayInvoice(bolt11, payParams.Amount?.MilliSatoshi);
|
||||
}
|
||||
|
@ -201,7 +169,8 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
return await Pay(bolt11, new PayInvoiceParams(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
@ -211,7 +180,8 @@ public class BTCPayAppLightningClient:ILightningClient
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = new CancellationToken())
|
||||
public async Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using BTCPayApp.CommonServer;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.App;
|
||||
|
||||
public class BTCPayAppLightningConnectionStringHandler:ILightningConnectionStringHandler
|
||||
{
|
||||
private readonly IHubContext<BTCPayAppHub, IBTCPayAppHubClient> _hubContext;
|
||||
private readonly BTCPayAppState _appState;
|
||||
private readonly DefaultHubLifetimeManager<BTCPayAppHub> _lifetimeManager;
|
||||
|
||||
public BTCPayAppLightningConnectionStringHandler(IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext, BTCPayAppState appState)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_appState = appState;
|
||||
}
|
||||
|
||||
public ILightningClient Create(string connectionString, Network network, [UnscopedRef] out string error)
|
||||
{
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type);
|
||||
if (type != "app")
|
||||
{
|
||||
error = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!kv.TryGetValue("group", out var key))
|
||||
{
|
||||
error = $"The key 'group' is mandatory for app connection strings";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!_appState.NodeToConnectionId.TryGetValue(key, out var connectionId) || !_appState.GroupToConnectionId.TryGetValues(key, out var conns) || !conns.Contains(connectionId))
|
||||
{
|
||||
error = $"The group {key} is not connected";
|
||||
return null;
|
||||
}
|
||||
error = null;
|
||||
return new BTCPayAppLightningClient(_hubContext, _appState, key );
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ using BTCPayServer.Lightning;
|
|||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
@ -24,6 +25,7 @@ namespace BTCPayServer.Controllers;
|
|||
|
||||
public class BTCPayAppState : IHostedService
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IHubContext<BTCPayAppHub, IBTCPayAppHubClient> _hubContext;
|
||||
private readonly ILogger<BTCPayAppState> _logger;
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
|
@ -34,18 +36,26 @@ public class BTCPayAppState : IHostedService
|
|||
public ExplorerClient ExplorerClient { get; private set; }
|
||||
|
||||
private DerivationSchemeParser _derivationSchemeParser;
|
||||
public readonly ConcurrentDictionary<string, string> GroupToConnectionId = new(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public readonly ConcurrentMultiDictionary<string, string> GroupToConnectionId =
|
||||
new(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public readonly ConcurrentDictionary<string, string> NodeToConnectionId =
|
||||
new(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public event EventHandler<(string, LightningInvoice)>? OnInvoiceUpdate;
|
||||
|
||||
public BTCPayAppState(
|
||||
StoreRepository storeRepository,
|
||||
IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext,
|
||||
ILogger<BTCPayAppState> logger,
|
||||
ExplorerClientProvider explorerClientProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
EventAggregator eventAggregator, IServiceProvider serviceProvider)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_hubContext = hubContext;
|
||||
_logger = logger;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
|
@ -62,7 +72,8 @@ public class BTCPayAppState : IHostedService
|
|||
_compositeDisposable = new CompositeDisposable();
|
||||
_compositeDisposable.Add(_eventAggregator.Subscribe<NewBlockEvent>(OnNewBlock));
|
||||
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<NewOnChainTransactionEvent>(OnNewTransaction));
|
||||
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<UserNotificationsUpdatedEvent>(UserNotificationsUpdatedEvent));
|
||||
_compositeDisposable.Add(
|
||||
_eventAggregator.SubscribeAsync<UserNotificationsUpdatedEvent>(UserNotificationsUpdatedEvent));
|
||||
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<InvoiceEvent>(InvoiceChangedEvent));
|
||||
// Store events
|
||||
_compositeDisposable.Add(_eventAggregator.SubscribeAsync<StoreCreatedEvent>(StoreCreatedEvent));
|
||||
|
@ -77,54 +88,58 @@ public class BTCPayAppState : IHostedService
|
|||
|
||||
private async Task InvoiceChangedEvent(InvoiceEvent arg)
|
||||
{
|
||||
await _hubContext.Clients.Group(arg.Invoice.StoreId).NotifyServerEvent(new ServerEvent("invoice-updated") { StoreId = arg.Invoice.StoreId, InvoiceId = arg.InvoiceId });
|
||||
await _hubContext.Clients.Group(arg.Invoice.StoreId).NotifyServerEvent(
|
||||
new ServerEvent("invoice-updated") {StoreId = arg.Invoice.StoreId, InvoiceId = arg.InvoiceId});
|
||||
}
|
||||
|
||||
private async Task UserNotificationsUpdatedEvent(UserNotificationsUpdatedEvent arg)
|
||||
{
|
||||
await _hubContext.Clients.Group(arg.UserId).NotifyServerEvent(new ServerEvent("notifications-updated") { UserId = arg.UserId });
|
||||
await _hubContext.Clients.Group(arg.UserId)
|
||||
.NotifyServerEvent(new ServerEvent("notifications-updated") {UserId = arg.UserId});
|
||||
}
|
||||
|
||||
private async Task StoreCreatedEvent(StoreCreatedEvent arg)
|
||||
{
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(new ServerEvent("store-created") { StoreId = arg.StoreId });
|
||||
await _hubContext.Clients.Group(arg.StoreId)
|
||||
.NotifyServerEvent(new ServerEvent("store-created") {StoreId = arg.StoreId});
|
||||
}
|
||||
|
||||
private async Task StoreUpdatedEvent(StoreUpdatedEvent arg)
|
||||
{
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(new ServerEvent("store-updated") { StoreId = arg.StoreId });
|
||||
await _hubContext.Clients.Group(arg.StoreId)
|
||||
.NotifyServerEvent(new ServerEvent("store-updated") {StoreId = arg.StoreId});
|
||||
}
|
||||
|
||||
private async Task StoreRemovedEvent(StoreRemovedEvent arg)
|
||||
{
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(new ServerEvent("store-removed") { StoreId = arg.StoreId });
|
||||
await _hubContext.Clients.Group(arg.StoreId)
|
||||
.NotifyServerEvent(new ServerEvent("store-removed") {StoreId = arg.StoreId});
|
||||
}
|
||||
|
||||
private async Task StoreUserAddedEvent(UserStoreAddedEvent arg)
|
||||
{
|
||||
var ev = new ServerEvent("user-store-added") { StoreId = arg.StoreId, UserId = arg.UserId };
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev);
|
||||
await _hubContext.Clients.Group(arg.UserId).NotifyServerEvent(ev);
|
||||
if (GroupToConnectionId.TryGetValues(arg.UserId, out var connectionIdsForUser))
|
||||
{
|
||||
await AddToGroup(arg.StoreId, connectionIdsForUser);
|
||||
}
|
||||
|
||||
// TODO: Add user to store group - how to get the connectionId?
|
||||
//await _hubContext.Groups.AddToGroupAsync(connectionId, arg.StoreId);
|
||||
var ev = new ServerEvent("user-store-added") {StoreId = arg.StoreId, UserId = arg.UserId};
|
||||
await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev);
|
||||
}
|
||||
|
||||
private async Task StoreUserUpdatedEvent(UserStoreUpdatedEvent arg)
|
||||
{
|
||||
var ev = new ServerEvent("user-store-updated") {StoreId = arg.StoreId, UserId = arg.UserId};
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev);
|
||||
await _hubContext.Clients.Group(arg.UserId).NotifyServerEvent(ev);
|
||||
await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev);
|
||||
}
|
||||
|
||||
private async Task StoreUserRemovedEvent(UserStoreRemovedEvent arg)
|
||||
{
|
||||
var ev = new ServerEvent("user-store-removed") {StoreId = arg.StoreId, UserId = arg.UserId};
|
||||
await _hubContext.Clients.Group(arg.StoreId).NotifyServerEvent(ev);
|
||||
await _hubContext.Clients.Group(arg.UserId).NotifyServerEvent(ev);
|
||||
await _hubContext.Clients.Groups(arg.StoreId, arg.UserId).NotifyServerEvent(ev);
|
||||
|
||||
// TODO: Remove user from store group - how to get the connectionId?
|
||||
//await _hubContext.Groups.RemoveFromGroupAsync(connectionId, arg.UserId);
|
||||
if (GroupToConnectionId.TryGetValues(arg.UserId, out var connectionIdsForUser))
|
||||
await RemoveFromGroup(arg.StoreId, connectionIdsForUser);
|
||||
}
|
||||
|
||||
private string _nodeInfo = string.Empty;
|
||||
|
@ -210,10 +225,6 @@ public class BTCPayAppState : IHostedService
|
|||
{
|
||||
if (TrackedSource.TryParse(ts, out var trackedSource, ExplorerClient.Network))
|
||||
{
|
||||
if (trackedSource is GroupTrackedSource groupTrackedSource)
|
||||
{
|
||||
}
|
||||
|
||||
ExplorerClient.Track(trackedSource);
|
||||
}
|
||||
|
||||
|
@ -258,19 +269,29 @@ public class BTCPayAppState : IHostedService
|
|||
{
|
||||
if (active)
|
||||
{
|
||||
if (GroupToConnectionId.TryAdd(group, contextConnectionId))
|
||||
if (NodeToConnectionId.TryGetValue(group, out var existingConnectionId))
|
||||
{
|
||||
return existingConnectionId == contextConnectionId;
|
||||
}
|
||||
|
||||
if (GroupToConnectionId.ContainsValue(group, contextConnectionId) &&
|
||||
NodeToConnectionId.TryAdd(group, contextConnectionId))
|
||||
{
|
||||
var connString ="type=app;group=" + group;
|
||||
_serviceProvider.GetService<LightningListener>()?.CheckConnection(ExplorerClient.CryptoCode, connString);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Should we check if the node actually works? Probably not as this call happens before the node is actually started
|
||||
// var connString ="type=app;group=" + group;
|
||||
// _serviceProvider.GetService<LightningListener>()?.CheckConnection(ExplorerClient.CryptoCode, connString);
|
||||
}
|
||||
|
||||
if (GroupToConnectionId.TryGetValue(group, out var connId) && connId == contextConnectionId)
|
||||
else
|
||||
{
|
||||
return GroupToConnectionId.TryRemove(group, out _);
|
||||
if (NodeToConnectionId.TryGetValue(group, out var existingConnectionId) &&
|
||||
existingConnectionId == contextConnectionId)
|
||||
{
|
||||
NodeToConnectionId.TryRemove(group, out _);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -278,27 +299,61 @@ public class BTCPayAppState : IHostedService
|
|||
|
||||
public async Task Disconnected(string contextConnectionId)
|
||||
{
|
||||
foreach (var group in GroupToConnectionId.Where(a => a.Value == contextConnectionId).Select(a => a.Key)
|
||||
.ToArray())
|
||||
GroupToConnectionId.RemoveValue(contextConnectionId, out var groupsRemoved);
|
||||
Array.ForEach(groupsRemoved, group =>
|
||||
{
|
||||
if (GroupToConnectionId.TryRemove(group, out _))
|
||||
GroupRemoved?.Invoke(this, group);
|
||||
});
|
||||
}
|
||||
|
||||
public event EventHandler<string>? GroupRemoved;
|
||||
|
||||
public async Task Connected(string contextConnectionId, string userId)
|
||||
{
|
||||
if (_nodeInfo.Length > 0)
|
||||
await _hubContext.Clients.Client(contextConnectionId).NotifyServerNode(_nodeInfo);
|
||||
|
||||
await _hubContext.Clients.Client(contextConnectionId)
|
||||
.NotifyNetwork(_networkProvider.BTC.NBitcoinNetwork.ToString());
|
||||
var userStores = await _storeRepository.GetStoresByUserId(userId);
|
||||
await AddToGroup(contextConnectionId, userId);
|
||||
foreach (var userStore in userStores)
|
||||
{
|
||||
await AddToGroup(contextConnectionId, userStore.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InvoiceUpdate(string identifier, LightningInvoice lightningInvoice)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
$"Invoice update for {identifier} {lightningInvoice.Amount} {lightningInvoice.PaymentHash}");
|
||||
OnInvoiceUpdate?.Invoke(this, (identifier, lightningInvoice));
|
||||
}
|
||||
|
||||
//what are we adding to groups?
|
||||
//user id
|
||||
//store id(s)
|
||||
//tracked sources
|
||||
|
||||
public async Task AddToGroup(string group, params string[] connectionIds)
|
||||
{
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
await _hubContext.Groups.AddToGroupAsync(connectionId, group);
|
||||
GroupToConnectionId.Add(group, connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveFromGroup(string group, params string[] connectionIds)
|
||||
{
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
await _hubContext.Groups.RemoveFromGroupAsync(connectionId, group);
|
||||
GroupToConnectionId.Remove(group, connectionId, out var keyRemoved);
|
||||
if (keyRemoved)
|
||||
{
|
||||
GroupRemoved?.Invoke(this, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<string>? GroupRemoved;
|
||||
|
||||
public async Task Connected(string contextConnectionId)
|
||||
{
|
||||
if (_nodeInfo.Length > 0)
|
||||
await _hubContext.Clients.Client(contextConnectionId).NotifyServerNode(_nodeInfo);
|
||||
}
|
||||
|
||||
public async Task InvoiceUpdate(string identifier, LightningInvoice lightningInvoice)
|
||||
{
|
||||
_logger.LogInformation($"Invoice update for {identifier} {lightningInvoice.Amount} {lightningInvoice.PaymentHash}");
|
||||
OnInvoiceUpdate?.Invoke(this, (identifier, lightningInvoice));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\BTCPayApp.VSS\BTCPayApp.VSS.csproj" />
|
||||
<ProjectReference Include="..\BTCPayApp.CommonServer\BTCPayApp.CommonServer.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
|
|
|
@ -71,9 +71,7 @@ using BTCPayServer.Payments.LNURLPay;
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Payouts;
|
||||
using ExchangeSharp;
|
||||
|
||||
|
||||
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
|
||||
|
||||
#if ALTCOINS
|
||||
|
@ -97,6 +95,8 @@ namespace BTCPayServer.Hosting
|
|||
{
|
||||
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
|
||||
factory.ConfigureBuilder(o);
|
||||
|
||||
o.UsePostgreSqlTriggers();
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.AddHttpClient(nameof(ExplorerClientProvider), httpClient =>
|
||||
|
|
Loading…
Add table
Reference in a new issue