backup store

This commit is contained in:
Kukks 2024-07-25 14:51:15 +02:00
parent deb9c1e1e1
commit fcd365b8f6
No known key found for this signature in database
GPG key ID: 8E5530D9D1C93097
21 changed files with 3650 additions and 182 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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