mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-16 23:24:25 +01:00
micronode
This commit is contained in:
@@ -55,10 +55,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Dynami
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Bringin", "Plugins\BTCPayServer.Plugins.Bringin\BTCPayServer.Plugins.Bringin.csproj", "{D4AFEC95-64D4-4FC4-9AE4-B82F4C6D6E29}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.LDK", "Plugins\BTCPayServer.Plugins.LDK\BTCPayServer.Plugins.LDK.csproj", "{661DBF95-0F60-49C0-829A-C5997B44AF60}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Blink", "Plugins\BTCPayServer.Plugins.Blink\BTCPayServer.Plugins.Blink.csproj", "{C0714C5C-1798-4576-9892-3CE097FDE76F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.MicroNode", "Plugins\BTCPayServer.Plugins.MicroNode\BTCPayServer.Plugins.MicroNode.csproj", "{95626F3B-7722-4AE7-9C12-EDB1E58687E2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -267,14 +267,6 @@ Global
|
||||
{5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5934F898-00B1-4781-BD18-04DF8685BC76}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{661DBF95-0F60-49C0-829A-C5997B44AF60}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -283,6 +275,14 @@ Global
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C0714C5C-1798-4576-9892-3CE097FDE76F}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95626F3B-7722-4AE7-9C12-EDB1E58687E2}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
||||
|
||||
@@ -10,8 +10,18 @@ foreach (var plugin in plugins)
|
||||
var assemblyConfigurationAttribute = typeof(Program).Assembly.GetCustomAttribute<AssemblyConfigurationAttribute>();
|
||||
var buildConfigurationName = assemblyConfigurationAttribute?.Configuration;
|
||||
var x = Directory.GetDirectories(Path.Combine(plugin, "bin"));
|
||||
|
||||
var f = $"{Path.GetFullPath(plugin)}/bin/{buildConfigurationName}/net8.0/{Path.GetFileName(plugin)}.dll";
|
||||
if (File.Exists(f))
|
||||
p += $"{f};";
|
||||
else
|
||||
{
|
||||
|
||||
f = $"{Path.GetFullPath(plugin)}/bin/Debug/net8.0/{Path.GetFileName(plugin)}.dll";
|
||||
if (File.Exists(f))
|
||||
p += $"{f};";
|
||||
}
|
||||
|
||||
p += $"{Path.GetFullPath(plugin)}/bin/{buildConfigurationName}/net8.0/{Path.GetFileName(plugin)}.dll;";
|
||||
// if (x.Any(s => s.EndsWith("Altcoins-Debug")))
|
||||
// {
|
||||
// p += $"{Path.GetFullPath(plugin)}/bin/Altcoins-Debug/net8.0/{Path.GetFileName(plugin)}.dll;";
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Plugin specific properties -->
|
||||
<PropertyGroup>
|
||||
<Product>MicroNode</Product>
|
||||
<Description>Micro ln node.</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
<PackageReference Include="AsyncKeyedLock" Version="6.2.6" />
|
||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
||||
<PackageReference Include="Laraue.EfCoreTriggers.PostgreSql" Version="8.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
13
Plugins/BTCPayServer.Plugins.MicroNode/MicroAccount.cs
Normal file
13
Plugins/BTCPayServer.Plugins.MicroNode/MicroAccount.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroAccount
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public long Balance { get; set; }
|
||||
public long BalanceCheckpoint { get; set; }
|
||||
public string MasterStoreId { get; set; }
|
||||
|
||||
public List<MicroTransaction> Transactions { get; set; } = new List<MicroTransaction>();
|
||||
}
|
||||
264
Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs
Normal file
264
Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroLightningClient:ILightningClient
|
||||
{
|
||||
private readonly ILightningClient _innerClient;
|
||||
private readonly MicroNodeService _microNodeService;
|
||||
private readonly Network _network;
|
||||
private readonly string _key;
|
||||
|
||||
public MicroLightningClient(ILightningClient innerClient,MicroNodeService microNodeService, Network network ,string key)
|
||||
{
|
||||
_innerClient = innerClient;
|
||||
_microNodeService = microNodeService;
|
||||
_network = network;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"type=micro;key={_key}";
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _microNodeService.MatchRecord(_key, invoiceId);
|
||||
if(result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _innerClient.GetInvoice(invoiceId, cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(uint256 paymentHash, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await GetInvoice(paymentHash.ToString(), cancellation);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _innerClient.ListInvoices(cancellation);
|
||||
var matchedRecords = await _microNodeService.MatchRecords(_key, result?.Select(r => r.Id).ToArray());
|
||||
var ids = matchedRecords.Select(r => r.Id).ToArray();
|
||||
return result.Where(r => ids.Contains(r.Id)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice[]> ListInvoices(ListInvoicesParams request, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _innerClient.ListInvoices(request, cancellation);
|
||||
var matchedRecords = await _microNodeService.MatchRecords(_key, result?.Select(r => r.Id).ToArray());
|
||||
var ids = matchedRecords.Select(r => r.Id).ToArray();
|
||||
return result.Where(r => ids.Contains(r.Id)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment> GetPayment(string paymentHash, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _microNodeService.MatchRecord(_key, paymentHash);
|
||||
if(result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//
|
||||
// if(result.Type == "InternalPayment")
|
||||
// {
|
||||
// return FromInternalPayment(result);
|
||||
// }
|
||||
var payment = await _innerClient.GetPayment(paymentHash, cancellation);
|
||||
await _microNodeService.UpsertRecord(_key, payment);
|
||||
return payment;
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _innerClient.ListPayments(cancellation);
|
||||
var matchedRecords = await _microNodeService.MatchRecords(_key, result?.Select(r => r.Id).ToArray());
|
||||
|
||||
// var internalPayments = matchedRecords.Where(r => r.Type == "InternalPayment").Select(FromInternalPayment).ToArray();
|
||||
var ids = matchedRecords.Select(r => r.Id).ToArray();
|
||||
var payments= result.Where(r => ids.Contains(r.Id)).ToArray();
|
||||
await _microNodeService.UpsertRecords(_key, payments);
|
||||
return payments;
|
||||
// return payments.Concat(internalPayments).ToArray();
|
||||
}
|
||||
|
||||
public async Task<LightningPayment[]> ListPayments(ListPaymentsParams request, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _innerClient.ListPayments(request, cancellation);
|
||||
var matchedRecords = await _microNodeService.MatchRecords(_key, result?.Select(r => r.Id).ToArray());
|
||||
// var internalPayments = matchedRecords.Where(r => r.Type == "InternalPayment").Select(FromInternalPayment).ToArray();
|
||||
var ids = matchedRecords.Select(r => r.Id).ToArray();
|
||||
var payments= result.Where(r => ids.Contains(r.Id)).ToArray();
|
||||
await _microNodeService.UpsertRecords(_key, payments);
|
||||
return payments;
|
||||
// return payments.Concat(internalPayments).ToArray();
|
||||
}
|
||||
|
||||
// public LightningPayment FromInternalPayment(MicroTransaction transaction)
|
||||
// {
|
||||
// if(transaction.Type != "InternalPayment")
|
||||
// throw new InvalidOperationException();
|
||||
// return new LightningPayment()
|
||||
// {
|
||||
// //balance is in - so convert it to positive
|
||||
// Amount = new LightMoney(Math.Abs(transaction.Amount)),
|
||||
// Fee = LightMoney.Zero,
|
||||
// Id = transaction.Id,
|
||||
// Status = !transaction.Active && transaction.Accounted ? LightningPaymentStatus.Complete :
|
||||
// transaction.Active ? LightningPaymentStatus.Pending : LightningPaymentStatus.Failed,
|
||||
// AmountSent = new LightMoney(Math.Abs(transaction.Amount)),
|
||||
// PaymentHash = transaction.Id
|
||||
// };
|
||||
// }
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var invoice = await _innerClient.CreateInvoice(amount, description, expiry, cancellation);
|
||||
await _microNodeService.UpsertRecord(_key, invoice);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(CreateInvoiceParams createInvoiceRequest, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var invoice = await _innerClient.CreateInvoice(createInvoiceRequest, cancellation);
|
||||
await _microNodeService.UpsertRecord(_key, invoice);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
|
||||
public class MicroListener: ILightningInvoiceListener
|
||||
{
|
||||
private readonly ILightningInvoiceListener _inner;
|
||||
private readonly MicroNodeService _microNodeService;
|
||||
private readonly string _key;
|
||||
|
||||
public MicroListener(ILightningInvoiceListener inner, MicroNodeService microNodeService, string key)
|
||||
{
|
||||
_inner = inner;
|
||||
_microNodeService = microNodeService;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var invoice = await _inner.WaitInvoice(cancellation);
|
||||
var record = await _microNodeService.MatchRecord(_key, invoice.Id);
|
||||
if(record is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await _microNodeService.UpsertRecord(_key, invoice);
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_inner.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<ILightningInvoiceListener> Listen(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return new MicroListener(await _innerClient.Listen(cancellation), _microNodeService, _key);
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var info = await _innerClient.GetInfo(cancellation);
|
||||
return info;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeBalance> GetBalance(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return new LightningNodeBalance(null, new OffchainBalance()
|
||||
{
|
||||
Local = await _microNodeService.GetBalance(_key, cancellation)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
var invoice = BOLT11PaymentRequest.Parse(bolt11, _network);
|
||||
var id = payParams.PaymentHash?.ToString() ?? invoice.PaymentHash.ToString();
|
||||
var amount = payParams?.Amount?? invoice.MinimumAmount;
|
||||
var record = await _microNodeService.InitiatePayment(_key, id,amount,
|
||||
cancellation);
|
||||
var result = await _innerClient.Pay(bolt11,payParams, cancellation);
|
||||
await _microNodeService.UpsertRecord(_key, FromPayResponse(result, record));
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PayResponse> Pay(string bolt11, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
return await Pay(bolt11, null, cancellation);
|
||||
}
|
||||
|
||||
private LightningPayment FromPayResponse(PayResponse payResponse, MicroTransaction transaction)
|
||||
{
|
||||
return new LightningPayment()
|
||||
{
|
||||
Id = transaction.Id,
|
||||
PaymentHash = transaction.Id,
|
||||
Preimage = payResponse.Details?.Preimage?.ToString(),
|
||||
Fee = payResponse.Details?.FeeAmount,
|
||||
Amount = LightMoney.MilliSatoshis(Math.Abs(transaction.Amount)),
|
||||
CreatedAt = null,
|
||||
Status = payResponse.Result switch
|
||||
{
|
||||
PayResult.Ok => LightningPaymentStatus.Complete,
|
||||
PayResult.Unknown => LightningPaymentStatus.Unknown,
|
||||
PayResult.CouldNotFindRoute => LightningPaymentStatus.Failed,
|
||||
PayResult.Error => LightningPaymentStatus.Failed,
|
||||
_ => LightningPaymentStatus.Unknown
|
||||
},
|
||||
AmountSent = transaction.Amount
|
||||
};
|
||||
}
|
||||
|
||||
public Task<OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public Task<BitcoinAddress> GetDepositAddress(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public Task<ConnectionResult> ConnectTo(NodeInfo nodeInfo, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public async Task CancelInvoice(string invoiceId, CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
var result = await _microNodeService.MatchRecord(_key, invoiceId);
|
||||
if(result is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _innerClient.CancelInvoice(invoiceId, cancellation);
|
||||
}
|
||||
|
||||
public Task<LightningChannel[]> ListChannels(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroLightningConnectionStringHandler : ILightningConnectionStringHandler
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
|
||||
public MicroLightningConnectionStringHandler(
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
public ILightningClient Create(string connectionString, Network network, out string error)
|
||||
{
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(connectionString, out var type);
|
||||
if (type != "micro" || kv is null)
|
||||
{
|
||||
error = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_networkProvider.BTC.NBitcoinNetwork != network)
|
||||
{
|
||||
error = "Invalid network";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!kv.TryGetValue("key", out var key))
|
||||
{
|
||||
error = "key is missing";
|
||||
return null;
|
||||
}
|
||||
var microNodeService = _serviceProvider.GetService<MicroNodeService>();
|
||||
|
||||
var settings = microNodeService.GetMasterSettingsFromKey(key).GetAwaiter().GetResult();
|
||||
|
||||
if (settings is null)
|
||||
{
|
||||
error = "key is invalid";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (settings.Value.Item1.Enabled is not true)
|
||||
{
|
||||
error = "MicroBank is not enabled";
|
||||
return null;
|
||||
}
|
||||
|
||||
var lightningClient = microNodeService.GetMasterLightningClient(settings.Value.Item2).GetAwaiter().GetResult();
|
||||
if (lightningClient is null)
|
||||
{
|
||||
error = "Lightning node not available";
|
||||
return null;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return new MicroLightningClient(lightningClient, microNodeService, network, key);
|
||||
}
|
||||
}
|
||||
201
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeContext.cs
Normal file
201
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeContext.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using Laraue.EfCoreTriggers.Common.Extensions;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MicroNodeContext>
|
||||
{
|
||||
public MicroNodeContext CreateDbContext(string[] args)
|
||||
{
|
||||
DbContextOptionsBuilder<MicroNodeContext> builder = new DbContextOptionsBuilder<MicroNodeContext>();
|
||||
|
||||
// FIXME: Somehow the DateTimeOffset column types get messed up when not using Postgres
|
||||
// https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli
|
||||
builder.UseNpgsql("User ID=postgres;Host=127.0.0.1;Port=39372;Database=designtimebtcpay");
|
||||
builder.UsePostgreSqlTriggers();
|
||||
return new MicroNodeContext(builder.Options);
|
||||
}
|
||||
}
|
||||
|
||||
public class MicroNodeContext : DbContext
|
||||
{
|
||||
public DbSet<MicroTransaction> MicroTransactions { get; set; }
|
||||
public DbSet<MicroAccount> MicroAccounts { get; set; }
|
||||
|
||||
|
||||
public MicroNodeContext()
|
||||
{
|
||||
}
|
||||
|
||||
public MicroNodeContext(DbContextOptions<MicroNodeContext> builderOptions) : base(builderOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.HasDefaultSchema("BTCPayServer.Plugins.MicroNode");
|
||||
modelBuilder.Entity<MicroTransaction>()
|
||||
.HasKey(t => new {t.Id, t.AccountId});
|
||||
modelBuilder.Entity<MicroAccount>()
|
||||
.HasKey(t => t.Key);
|
||||
modelBuilder.Entity<MicroAccount>()
|
||||
.HasMany<MicroTransaction>(account => account.Transactions)
|
||||
.WithOne(transaction => transaction.Account)
|
||||
.HasForeignKey(transaction => transaction.AccountId);
|
||||
|
||||
modelBuilder.Entity<MicroTransaction>()
|
||||
.HasOne<MicroTransaction>().WithMany(transaction => transaction.Dependents)
|
||||
.HasForeignKey(transaction => new {transaction.DependentId, transaction.AccountId})
|
||||
.HasPrincipalKey(transaction => new {transaction.Id, transaction.AccountId})
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<MicroTransaction>()
|
||||
.AfterInsert(trigger => trigger
|
||||
.Action(action =>
|
||||
{
|
||||
action.Condition(@ref => @ref.New.Accounted)
|
||||
.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) =>
|
||||
microAccount.Key ==
|
||||
tableRefs.New.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{Balance = oldBalance.Balance + tableRefs.New.Amount});
|
||||
})
|
||||
.Action(action =>
|
||||
{
|
||||
action.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) =>
|
||||
microAccount.Key ==
|
||||
tableRefs.New.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{BalanceCheckpoint = oldBalance.BalanceCheckpoint + 1});
|
||||
})
|
||||
.Action(action =>
|
||||
{
|
||||
action.Update<MicroTransaction>(
|
||||
(tableRefs, tx) =>
|
||||
tableRefs.New.Id == tx.DependentId && tx.AccountId ==
|
||||
tableRefs.New.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, tx) => new MicroTransaction()
|
||||
{Accounted = tableRefs.New.Accounted, Active = tableRefs.New.Active});
|
||||
}));
|
||||
|
||||
modelBuilder.Entity<MicroTransaction>()
|
||||
.AfterDelete(trigger => trigger
|
||||
.Action(action =>
|
||||
{
|
||||
action.Condition(@ref => @ref.Old.Accounted)
|
||||
.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) =>
|
||||
microAccount.Key ==
|
||||
tableRefs.Old.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{Balance = oldBalance.Balance - tableRefs.Old.Amount});
|
||||
})
|
||||
.Action(action =>
|
||||
{
|
||||
action.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) =>
|
||||
microAccount.Key ==
|
||||
tableRefs.Old.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{BalanceCheckpoint = oldBalance.BalanceCheckpoint + 1});
|
||||
}));
|
||||
|
||||
modelBuilder.Entity<MicroTransaction>()
|
||||
.AfterUpdate(trigger => trigger
|
||||
.Action(action =>
|
||||
{
|
||||
action.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) =>
|
||||
microAccount.Key ==
|
||||
tableRefs.Old.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{BalanceCheckpoint = oldBalance.BalanceCheckpoint + 1});
|
||||
})
|
||||
.Action(action =>
|
||||
{
|
||||
action.Update<MicroTransaction>(
|
||||
(tableRefs, tx) =>
|
||||
tableRefs.Old.Id == tx.DependentId && tx.AccountId ==
|
||||
tableRefs.New.AccountId, // Will be updated entities with matched condition
|
||||
(tableRefs, tx) => new MicroTransaction()
|
||||
{
|
||||
Accounted = tableRefs.New.Accounted, Active = tableRefs.New.Active,
|
||||
DependentId = tableRefs.New.Id
|
||||
});
|
||||
})
|
||||
|
||||
// Scenario 1: Transaction is newly accounted (not previously accounted)
|
||||
.Action(action =>
|
||||
{
|
||||
action.Condition(@ref => @ref.New.Accounted && !@ref.Old.Accounted)
|
||||
.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) => microAccount.Key == tableRefs.Old.AccountId,
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{
|
||||
Balance = oldBalance.Balance + tableRefs.New.Amount
|
||||
});
|
||||
})
|
||||
|
||||
// Scenario 2: Transaction was previously accounted and remains accounted (with a potential change in amount)
|
||||
.Action(action =>
|
||||
{
|
||||
action.Condition(@ref =>
|
||||
@ref.New.Accounted && @ref.Old.Accounted && @ref.Old.Amount != @ref.New.Amount)
|
||||
.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) => microAccount.Key == tableRefs.Old.AccountId,
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{
|
||||
Balance = oldBalance.Balance - tableRefs.Old.Amount + tableRefs.New.Amount
|
||||
});
|
||||
})
|
||||
|
||||
// Scenario 3: Transaction is unaccounted (previously accounted)
|
||||
.Action(action =>
|
||||
{
|
||||
action.Condition(@ref => !@ref.New.Accounted && @ref.Old.Accounted)
|
||||
.Update<MicroAccount>(
|
||||
(tableRefs, microAccount) => microAccount.Key == tableRefs.Old.AccountId,
|
||||
(tableRefs, oldBalance) => new MicroAccount
|
||||
{
|
||||
Balance = oldBalance.Balance - tableRefs.Old.Amount
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Scenario 4: Transaction state remains unchanged (neither accounted nor unaccounted)
|
||||
// Assuming no update is required in this case
|
||||
|
||||
|
||||
//unfortunately setting the balance this way is too complicated to generate the query
|
||||
// action.Condition(@ref => @ref.Old.Accounted != @ref.New.Accounted || @ref.Old.Amount != @ref.New.Amount)
|
||||
// .Update<MicroAccount>(
|
||||
// (tableRefs, microAccount) =>
|
||||
// microAccount.Id ==
|
||||
// tableRefs.Old.AccountId, // Will be updated entities with matched condition
|
||||
//
|
||||
// // we update the balance with a few dimensions:
|
||||
// // if the transaction was just accounted, we add the amount
|
||||
// // if the transaction was already accounted, we remove the old amount and add the new amount
|
||||
// // if the transaction was just unaccounted, we remove the amount
|
||||
// (tableRefs, oldBalance) => new MicroAccount
|
||||
// {
|
||||
// Balance =
|
||||
// tableRefs.New.Accounted && !tableRefs.Old.Accounted
|
||||
// ?
|
||||
// oldBalance.Balance + tableRefs.New.Amount
|
||||
// :
|
||||
// tableRefs.New.Accounted && tableRefs.Old.Accounted
|
||||
// ? oldBalance.Balance - tableRefs.Old.Amount + tableRefs.New.Amount
|
||||
// :
|
||||
// !tableRefs.New.Accounted && tableRefs.Old.Accounted
|
||||
// ? oldBalance.Balance - tableRefs.Old.Amount
|
||||
// : oldBalance.Balance
|
||||
// });
|
||||
// })); // New values for matched entities.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodeContextFactory : BaseDbContextFactory<MicroNodeContext>
|
||||
{
|
||||
public MicroNodeContextFactory(IOptions<DatabaseOptions> options) : base(options, "BTCPayServer.Plugins.MicroNode")
|
||||
{
|
||||
}
|
||||
|
||||
public override MicroNodeContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<MicroNodeContext>();
|
||||
ConfigureBuilder(builder);
|
||||
builder.UsePostgreSqlTriggers();
|
||||
|
||||
return new MicroNodeContext(builder.Options);
|
||||
}
|
||||
}
|
||||
208
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs
Normal file
208
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
[Route("plugins/micronode")]
|
||||
public class MicroNodeController : Controller
|
||||
{
|
||||
private readonly MicroNodeService _microNodeService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public MicroNodeController(MicroNodeService microNodeService, StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider, IAuthorizationService authorizationService)
|
||||
{
|
||||
_microNodeService = microNodeService;
|
||||
_storeRepository = storeRepository;
|
||||
_networkProvider = networkProvider;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpGet("configure/{storeId}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
||||
public async Task<IActionResult> Configure(string storeId)
|
||||
{
|
||||
var result = await _microNodeService.GetStoreSettings(storeId);
|
||||
if (result is not null)
|
||||
{
|
||||
var xy = await _microNodeService.GetMasterSettingsFromKey(result.Key);
|
||||
if (xy is not null)
|
||||
{
|
||||
HttpContext.Items.Add("MasterStoreId", xy.Value.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
return View(result);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("configure/{storeId}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
||||
public async Task<IActionResult> Configure(string storeId, string command, MicroNodeStoreSettings settings,
|
||||
string? masterStoreId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
if (network is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (masterStoreId == storeId)
|
||||
{
|
||||
ModelState.AddModelError("masterStoreId", "Master cannot be the same as this store");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
var existing = store.GetSupportedPaymentMethods(_networkProvider).OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(method =>
|
||||
method.PaymentId.PaymentType == LightningPaymentType.Instance &&
|
||||
method.PaymentId.CryptoCode == network.CryptoCode);
|
||||
var isSet = settings?.Key is not null;
|
||||
settings ??= new MicroNodeStoreSettings();
|
||||
settings.Key ??= Guid.NewGuid().ToString();
|
||||
var mlc = new MicroLightningClient(null, _microNodeService, network.NBitcoinNetwork,settings.Key);
|
||||
var isStoreSetToThisMicro = existing?.GetExternalLightningUrl() == mlc.ToString();
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
if (!isSet && masterStoreId is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Key), "A master was not selected");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
if (!isSet)
|
||||
{
|
||||
var masterSettings = await _microNodeService.GetMasterSettings(masterStoreId);
|
||||
if (masterSettings is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Key), "The master is not valid");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
if (!masterSettings.Enabled)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Key), "The master is not enabled");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
if (masterSettings.AdminOnly &&
|
||||
!(await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.Key),
|
||||
"The master is admin only and you are not an admin");
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
existing ??= new LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC"
|
||||
};
|
||||
existing.SetLightningUrl(mlc);
|
||||
store.SetSupportedPaymentMethod(existing);
|
||||
|
||||
|
||||
await _microNodeService.Set(storeId, settings, masterStoreId);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "MicroNode Set"
|
||||
});
|
||||
|
||||
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
case "clear":
|
||||
var ss = await _microNodeService.GetStoreSettings(storeId);
|
||||
if (ss is null)
|
||||
{
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
if (isStoreSetToThisMicro)
|
||||
{
|
||||
store.SetSupportedPaymentMethod(existing.PaymentId, null);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
}
|
||||
|
||||
await _microNodeService.SetMaster(storeId, null);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "MicroNode Cleared"
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Configure), new {storeId});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("configure-master/{storeId}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyServerSettings)]
|
||||
public async Task<IActionResult> ConfigureMaster(string storeId)
|
||||
{
|
||||
var result = await _microNodeService.GetMasterSettings(storeId);
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpPost("configure-master/{storeId}")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyServerSettings)]
|
||||
public async Task<IActionResult> ConfigureMaster(string storeId, string command, MicroNodeSettings settings)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
if (network is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (command == "clear")
|
||||
{
|
||||
await _microNodeService.SetMaster(storeId, (MicroNodeSettings) null);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "MicroNode Master Cleared"
|
||||
});
|
||||
return RedirectToAction(nameof(ConfigureMaster), new {storeId});
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(settings);
|
||||
}
|
||||
await _microNodeService.SetMaster(storeId, settings);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "MicroNode Master Update"
|
||||
});
|
||||
return RedirectToAction(nameof(ConfigureMaster), new {storeId});
|
||||
|
||||
}
|
||||
}
|
||||
40
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs
Normal file
40
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Lightning;
|
||||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodePlugin:BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new () { Identifier = nameof(BTCPayServer), Condition = ">=1.12.0" }
|
||||
};
|
||||
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("MicroNode/MicroNodeNav", "header-nav"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("MicroNode/LNPaymentMethodSetupTabhead", "ln-payment-method-setup-tabhead"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("MicroNode/LNPaymentMethodSetupTab", "ln-payment-method-setup-tab"));
|
||||
// applicationBuilder.AddStartupTask<MicroNodeStartupTask>();
|
||||
applicationBuilder.AddSingleton<MicroNodeContextFactory>();
|
||||
applicationBuilder.AddDbContext<MicroNodeContext>((provider, o) =>
|
||||
{
|
||||
var factory = provider.GetRequiredService<MicroNodeContextFactory>();
|
||||
factory.ConfigureBuilder(o);
|
||||
o.UsePostgreSqlTriggers();
|
||||
});
|
||||
applicationBuilder.AddSingleton<ILightningConnectionStringHandler, MicroLightningConnectionStringHandler>();
|
||||
applicationBuilder.AddSingleton<MicroNodeService>();
|
||||
applicationBuilder.AddHostedService(provider => provider.GetRequiredService<MicroNodeService>());
|
||||
|
||||
|
||||
base.Execute(applicationBuilder);
|
||||
|
||||
}
|
||||
}
|
||||
737
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeService.cs
Normal file
737
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeService.cs
Normal file
@@ -0,0 +1,737 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodeService : EventHostedServiceBase
|
||||
{
|
||||
private readonly MicroNodeContextFactory _microNodeContextFactory;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly ILogger<MicroNodeService> _logger;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
|
||||
private static readonly ConcurrentDictionary<string, long> ExpectedCounter = new();
|
||||
private readonly TaskCompletionSource _init = new();
|
||||
private Dictionary<string, MicroNodeSettings> _ownerSettings;
|
||||
private Dictionary<string, MicroNodeStoreSettings?> _storeSettings;
|
||||
private readonly BTCPayNetwork _network;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private static readonly AsyncKeyedLock.AsyncKeyedLocker<string> KeyedLocker = new();
|
||||
|
||||
public const string MasterSettingsKey = "MicroNodeMasterSettings";
|
||||
public const string StoreSettingsKey = "MicroNodeStoreSettings";
|
||||
|
||||
public MicroNodeService(MicroNodeContextFactory microNodeContextFactory,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
StoreRepository storeRepository,
|
||||
ILogger<MicroNodeService> logger,
|
||||
EventAggregator eventAggregator,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IServiceProvider serviceProvider) : base(eventAggregator, logger)
|
||||
{
|
||||
_network = btcPayNetworkProvider.BTC;
|
||||
_microNodeContextFactory = microNodeContextFactory;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_storeRepository = storeRepository;
|
||||
_logger = logger;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction?> MatchRecord(string key, string id)
|
||||
{
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var transaction = await ctx.MicroTransactions.FirstOrDefaultAsync(t => t.Id == id && t.AccountId == key);
|
||||
return transaction ?? null;
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction[]> MatchRecords(string key, string[]? toArray)
|
||||
{
|
||||
if (toArray is null)
|
||||
{
|
||||
return Array.Empty<MicroTransaction>();
|
||||
}
|
||||
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var transactions = await ctx.MicroTransactions.Where(t => toArray.Contains(t.Id) && t.AccountId == key)
|
||||
.ToArrayAsync();
|
||||
return transactions;
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction> UpsertRecord(string key, LightningInvoice invoice)
|
||||
{
|
||||
return (await UpsertRecords(key, new[] {invoice})).First();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Task<MicroTransaction[]> UpsertRecords(string key, LightningInvoice[] invoices)
|
||||
{
|
||||
return UpsertRecords(ConvertToRecords(key, invoices).ToArray());
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction> UpsertRecord(string key, Data.PayoutData payout)
|
||||
{
|
||||
return (await UpsertRecords(key, new[] {payout})).First();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private MicroTransaction[] ConvertToRecords(string key, LightningInvoice[] invoices)
|
||||
{
|
||||
return invoices.Select(invoice => new MicroTransaction()
|
||||
{
|
||||
Id = invoice.Id,
|
||||
AccountId = key,
|
||||
Amount = invoice.AmountReceived?.MilliSatoshi ?? invoice.Amount.MilliSatoshi,
|
||||
Accounted = invoice.Status == LightningInvoiceStatus.Paid,
|
||||
Type = "LightningInvoice",
|
||||
Active = invoice.Status == LightningInvoiceStatus.Unpaid
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private MicroTransaction[] ConvertToRecords(string key, Data.PayoutData[] payouts)
|
||||
{
|
||||
return payouts.SelectMany(payout =>
|
||||
{
|
||||
var b = payout.GetBlob(_btcPayNetworkJsonSerializerSettings);
|
||||
|
||||
List<MicroTransaction> res = new();
|
||||
res.Add(new MicroTransaction()
|
||||
{
|
||||
Id = payout.Id,
|
||||
AccountId = key,
|
||||
Amount = -LightMoney.Coins(b.CryptoAmount.Value).MilliSatoshi,
|
||||
Accounted = payout.State != PayoutState.Cancelled,
|
||||
Active = payout.State is PayoutState.AwaitingApproval or PayoutState.AwaitingPayment
|
||||
or PayoutState.InProgress,
|
||||
Type = "Payout"
|
||||
});
|
||||
|
||||
if (b.Metadata?.TryGetValue("Fee", out var microNode) is true && microNode.Value<decimal>() is { } payoutFee)
|
||||
{
|
||||
var fee = LightMoney.Coins(payoutFee);
|
||||
res.Add(new MicroTransaction()
|
||||
{
|
||||
Id = "FeeOf" + payout.Id,
|
||||
AccountId = key,
|
||||
Amount = -fee.MilliSatoshi,
|
||||
Accounted = payout.State != PayoutState.Cancelled,
|
||||
Active = payout.State is PayoutState.AwaitingApproval or PayoutState.AwaitingPayment
|
||||
or PayoutState.InProgress,
|
||||
Type = "PayoutFee"
|
||||
});
|
||||
}
|
||||
return res.ToArray();
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private MicroTransaction[] ConvertToRecords(string key, LightningPayment[] payments)
|
||||
{
|
||||
return payments.SelectMany(payment =>
|
||||
{
|
||||
var res = new List<MicroTransaction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = payment.Id,
|
||||
AccountId = key,
|
||||
Amount = -(payment.AmountSent?.MilliSatoshi ?? payment.Amount.MilliSatoshi),
|
||||
Accounted = payment.Status != LightningPaymentStatus.Failed,
|
||||
Type = "LightningPayment",
|
||||
Active = payment.Status is LightningPaymentStatus.Pending or LightningPaymentStatus.Unknown
|
||||
}
|
||||
};
|
||||
|
||||
if (payment.Fee is { } fee)
|
||||
{
|
||||
res.Add(new MicroTransaction()
|
||||
{
|
||||
Id = "FeeOf" + payment.Id,
|
||||
DependentId = payment.Id,
|
||||
AccountId = key,
|
||||
Amount = -fee.MilliSatoshi,
|
||||
Accounted = payment.Status != LightningPaymentStatus.Failed,
|
||||
Type = "LightningPaymentFee",
|
||||
Active = payment.Status is LightningPaymentStatus.Pending or LightningPaymentStatus.Unknown
|
||||
});
|
||||
}
|
||||
|
||||
return res.ToArray();
|
||||
}).ToArray();
|
||||
}
|
||||
public Task<MicroTransaction[]> UpsertRecords(string key, Data.PayoutData[] payouts)
|
||||
{
|
||||
return UpsertRecords(ConvertToRecords(key, payouts));
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction> UpsertRecord(string key, LightningPayment payment)
|
||||
{
|
||||
return (await UpsertRecords(key, new[] {payment})).First();
|
||||
}
|
||||
|
||||
public Task<MicroTransaction[]> UpsertRecords(string key, LightningPayment[] payments)
|
||||
{
|
||||
|
||||
|
||||
return UpsertRecords(ConvertToRecords(key, payments));
|
||||
}
|
||||
|
||||
private async Task<MicroTransaction[]> UpsertRecords(MicroTransaction[] transactions)
|
||||
{
|
||||
ExpectedCounter.TryGetValue(transactions.First().AccountId, out var expected);
|
||||
// expected += transactions.Length;
|
||||
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var cnt = await ctx.MicroTransactions.UpsertRange(transactions).RunAsync();
|
||||
ExpectedCounter[transactions.First().AccountId] = expected + cnt ;
|
||||
await ctx.SaveChangesAsync();
|
||||
return transactions;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<LightMoney?> GetBalance(string key, CancellationToken cancellation)
|
||||
{
|
||||
using var keylock = await KeyedLocker.LockAsync(key, cancellation);
|
||||
return await GetBalanceCore(key, cancellation);
|
||||
}
|
||||
|
||||
private async Task<LightMoney?> GetBalanceCore(string key, CancellationToken cancellation)
|
||||
{
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var account = await ctx.MicroAccounts.FindAsync(key);
|
||||
if (account is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ExpectedCounter.TryGetValue(key, out var expected))
|
||||
{
|
||||
if (account.BalanceCheckpoint < expected)
|
||||
{
|
||||
await Task.Delay(1000, cancellation);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExpectedCounter[key] = account.BalanceCheckpoint;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ExpectedCounter.TryAdd(key, account.BalanceCheckpoint);
|
||||
}
|
||||
|
||||
return new LightMoney(account.Balance, LightMoneyUnit.MilliSatoshi);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<(MicroNodeSettings, string)?> GetMasterSettingsFromKey(string key,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
await _init.Task.WaitAsync(cancellation);
|
||||
|
||||
if (_keyToMasterStoreId.TryGetValue(key, out var storeId))
|
||||
{
|
||||
return (_ownerSettings[storeId], storeId);
|
||||
}
|
||||
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var acct = await ctx.MicroAccounts.FindAsync(key, cancellation);
|
||||
if (acct is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var res = _ownerSettings.TryGetValue(acct.MasterStoreId, out var settings)
|
||||
? (settings, acct.MasterStoreId)
|
||||
: ((MicroNodeSettings settings, string MasterStoreId)?) null;
|
||||
|
||||
if (res is not null)
|
||||
{
|
||||
_keyToMasterStoreId.TryAdd(key, acct.MasterStoreId);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public async Task<MicroNodeSettings?> GetMasterSettings(string storeId, CancellationToken cancellation = default)
|
||||
{
|
||||
await _init.Task.WaitAsync(cancellation);
|
||||
return _ownerSettings.TryGetValue(storeId, out var settings) ? settings : null;
|
||||
}
|
||||
|
||||
public async Task<ImmutableDictionary<string, MicroNodeSettings>> GetMasterSettings(
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
await _init.Task.WaitAsync(cancellation);
|
||||
return _ownerSettings.Where(pair => pair.Value.Enabled).ToImmutableDictionary();
|
||||
}
|
||||
|
||||
public async Task<MicroNodeStoreSettings?> GetStoreSettings(string storeId,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
await _init.Task.WaitAsync(cancellation);
|
||||
return _storeSettings.TryGetValue(storeId, out var settings) ? settings : null;
|
||||
}
|
||||
|
||||
public async Task<MicroNodeStoreSettings?> GetStoreSettingsFromKey(string key,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
await _init.Task.WaitAsync(cancellation);
|
||||
var res = _storeSettings.FirstOrDefault(pair => pair.Value?.Key == key);
|
||||
return res.Value;
|
||||
}
|
||||
|
||||
public async Task<ILightningClient?> GetMasterLightningClientFromKey(string key)
|
||||
{
|
||||
var settings = await GetMasterSettingsFromKey(key);
|
||||
if (settings is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await GetMasterLightningClient(settings.Value.Item2);
|
||||
}
|
||||
|
||||
|
||||
public async Task<ILightningClient?> GetMasterLightningClient(string storeId)
|
||||
{
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lightningConnectionString = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(method => method.CryptoCode == _network.CryptoCode)?.CreateLightningClient(_network,
|
||||
_lightningNetworkOptions.Value, _serviceProvider.GetService<LightningClientFactoryService>());
|
||||
return lightningConnectionString;
|
||||
}
|
||||
|
||||
public async Task<MicroTransaction> InitiatePayment(string key, string paymentId, LightMoney amount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (amount <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot pay a negative amount");
|
||||
}
|
||||
using var locker = await KeyedLocker.LockAsync(key, cancellationToken);
|
||||
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var matched = await ctx.MicroTransactions.Include(transaction => transaction.Account).Where(transaction => transaction.Id == paymentId)
|
||||
.ToArrayAsync(cancellationToken: cancellationToken);
|
||||
|
||||
var ourRecord = matched.FirstOrDefault(transaction => transaction.AccountId == key);
|
||||
|
||||
// already successfully paid
|
||||
if(ourRecord is {Active: false, Accounted: true} && ourRecord.Amount == -amount.MilliSatoshi)
|
||||
{
|
||||
return ourRecord;
|
||||
//previous payment failure
|
||||
}else if(ourRecord is {Active: false, Accounted: false} && ourRecord.Amount == -amount.MilliSatoshi)
|
||||
{
|
||||
return ourRecord;
|
||||
}else if (ourRecord is not null)
|
||||
{
|
||||
throw new InvalidOperationException($"A record with this payment hash was already present- Accounted:{ourRecord.Accounted} Active:{ourRecord.Active} Amount:{ourRecord.Amount}");
|
||||
}
|
||||
|
||||
var masterStoreId = await GetMasterSettingsFromKey(key, cancellationToken);
|
||||
|
||||
var isInternal = matched.Any(transaction =>
|
||||
transaction.AccountId != key && transaction.Amount >= 0 &&
|
||||
transaction.Account.MasterStoreId == masterStoreId.Value.Item2);
|
||||
|
||||
var balance = SpendableExternalBalance(await GetBalanceCore(key, cancellationToken), isInternal, out var fee);
|
||||
|
||||
if(balance < amount)
|
||||
{
|
||||
throw new InvalidOperationException("Insufficient balance");
|
||||
}
|
||||
|
||||
var r = await UpsertRecords(new MicroTransaction[]
|
||||
{
|
||||
new MicroTransaction()
|
||||
{
|
||||
Id = paymentId,
|
||||
AccountId = key,
|
||||
Amount = -amount.MilliSatoshi,
|
||||
Accounted = true,
|
||||
Type = "LightningPayment",
|
||||
Active = true
|
||||
},
|
||||
new MicroTransaction()
|
||||
{
|
||||
Id = "FeeOf" + paymentId,
|
||||
AccountId = key,
|
||||
Amount = -fee.MilliSatoshi,
|
||||
Accounted = true,
|
||||
Type = "LightningPaymentFee",
|
||||
Active = true
|
||||
}
|
||||
});
|
||||
return r.First(transaction => transaction.Type == "LightningPayment");
|
||||
}
|
||||
|
||||
private LightMoney? SpendableExternalBalance(LightMoney? balance,bool isInternal, out LightMoney fee)
|
||||
{
|
||||
if (isInternal || balance < LightMoney.Satoshis(10000))
|
||||
{
|
||||
fee = LightMoney.Zero;
|
||||
return balance;
|
||||
}
|
||||
|
||||
var maxFeeAmount = (long)Math.Round(balance.MilliSatoshi * 0.03m);
|
||||
fee = LightMoney.MilliSatoshis(maxFeeAmount);
|
||||
return balance - fee;
|
||||
}
|
||||
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Migrating MicroNode database");
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var pendingMigrations = await ctx.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken);
|
||||
if (pendingMigrations.Any())
|
||||
{
|
||||
_logger.LogInformation("Applying {Count} migrations", pendingMigrations.Count());
|
||||
await ctx.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("No migrations to apply");
|
||||
}
|
||||
|
||||
_ownerSettings = await _storeRepository.GetSettingsAsync<MicroNodeSettings>(MasterSettingsKey);
|
||||
_storeSettings = await _storeRepository.GetSettingsAsync<MicroNodeStoreSettings>(StoreSettingsKey);
|
||||
_init.TrySetResult();
|
||||
PushEvent(new CheckActiveTransactions());
|
||||
PushEvent(new CreatePayoutEvt());
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
class CheckActiveTransactions;
|
||||
|
||||
class CreatePayoutEvt;
|
||||
|
||||
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is PayoutEvent payoutEvent)
|
||||
{
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var matchedTx = await ctx.MicroTransactions.SingleOrDefaultAsync(
|
||||
transaction => transaction.Amount < 0 && transaction.Id == payoutEvent.Payout.Id,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (matchedTx is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await UpsertRecord(matchedTx.AccountId, payoutEvent.Payout);
|
||||
}
|
||||
else if (evt is CheckActiveTransactions)
|
||||
{
|
||||
|
||||
Dictionary<string, ILightningClient> masterToLightningClients = new();
|
||||
foreach (var (key, value) in _ownerSettings)
|
||||
{
|
||||
//this simplistic approach should catch most cases
|
||||
if(!masterToLightningClients.TryGetValue(key, out var lnClient))
|
||||
{
|
||||
lnClient = await GetMasterLightningClient(key);
|
||||
if (lnClient is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
masterToLightningClients.Add(key, lnClient);
|
||||
}
|
||||
await lnClient.ListInvoices(cancellationToken);
|
||||
await lnClient.ListPayments(cancellationToken);
|
||||
}
|
||||
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var activeTransactions = await ctx.MicroTransactions.Where(transaction => transaction.Active)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
var transactionsPayouts = activeTransactions.Where(transaction => transaction.Type == "Payout").ToArray();
|
||||
|
||||
var payouts = await _pullPaymentHostedService.GetPayouts(new PullPaymentHostedService.PayoutQuery()
|
||||
{
|
||||
PayoutIds = transactionsPayouts.Select(p => p.Id).ToArray()
|
||||
});
|
||||
|
||||
var upsertRecords = new List<MicroTransaction>();
|
||||
foreach (var keyGroup in transactionsPayouts.GroupBy(transaction => transaction.AccountId))
|
||||
{
|
||||
var keyPayouts = payouts.Where(p => keyGroup.Any(transaction => p.Id == transaction.Id)).ToArray();
|
||||
upsertRecords.AddRange(ConvertToRecords(keyGroup.Key, keyPayouts));
|
||||
}
|
||||
|
||||
|
||||
var transactionsInvoices = activeTransactions.Where(transaction => transaction.Type == "LightningInvoice")
|
||||
.ToArray();
|
||||
|
||||
foreach (var keyGroup in transactionsInvoices.GroupBy(transaction => transaction.AccountId))
|
||||
{
|
||||
if (!masterToLightningClients.TryGetValue(keyGroup.Key, out var lightningClient))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await Task.WhenAll(keyGroup.Select(async transaction =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await lightningClient.GetInvoice(transaction.Id, cancellationToken);
|
||||
if (invoice is null && InvalidateAfterRetries(transaction.Id, 12))
|
||||
{
|
||||
transaction.Active = false;
|
||||
}else if(invoice is not null)
|
||||
{
|
||||
_retries.TryRemove(transaction.Id, out _);
|
||||
upsertRecords.AddRange(ConvertToRecords(keyGroup.Key, new []{invoice}));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (InvalidateAfterRetries(transaction.Id, 12))
|
||||
{
|
||||
transaction.Active = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
var transactionPayments = activeTransactions.Where(transaction => transaction.Type == "LightningPayment")
|
||||
.ToArray();
|
||||
|
||||
foreach (var keyGroup in transactionPayments.GroupBy(transaction => transaction.AccountId))
|
||||
{
|
||||
if (!masterToLightningClients.TryGetValue(keyGroup.Key, out var lightningClient))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await Task.WhenAll(keyGroup.Select(async transaction =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await lightningClient.GetPayment(transaction.Id, cancellationToken);
|
||||
if (invoice is null && InvalidateAfterRetries(transaction.Id, 60))
|
||||
{
|
||||
transaction.Active = false;
|
||||
}else if(invoice is not null)
|
||||
{
|
||||
_retries.TryRemove(transaction.Id, out _);
|
||||
upsertRecords.AddRange(ConvertToRecords(keyGroup.Key, new []{invoice}));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (InvalidateAfterRetries(transaction.Id, 60))
|
||||
{
|
||||
transaction.Active = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
_ = Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)).ContinueWith(
|
||||
task =>
|
||||
{
|
||||
if (!task.IsCanceled)
|
||||
PushEvent(new CheckActiveTransactions());
|
||||
}, cancellationToken);
|
||||
|
||||
return;
|
||||
}
|
||||
else if (evt is CreatePayoutEvt)
|
||||
{
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var autoForwardThreshold = LightMoney.Satoshis(10000).MilliSatoshi;
|
||||
var balances = await ctx.MicroAccounts.Where(account => account.Balance > autoForwardThreshold)
|
||||
.ToArrayAsync(cancellationToken: cancellationToken);
|
||||
foreach (var masterClients in balances.GroupBy(account => account.MasterStoreId))
|
||||
{
|
||||
var lnCLient = await GetMasterLightningClient(masterClients.Key);
|
||||
if (lnCLient is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var client in masterClients)
|
||||
{
|
||||
await KeyedLocker.TryLockAsync(client.Key, async () =>
|
||||
{
|
||||
var storeId = _storeSettings.FirstOrDefault(pair => pair.Value.Key == client.Key);
|
||||
var destination = storeId.Value?.ForwardDestination;
|
||||
if (destination is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var balance = await GetBalanceCore(client.Key, CancellationToken.None);
|
||||
if (balance is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var payout = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
{
|
||||
Value = LightMoney.MilliSatoshis(balance.MilliSatoshi).ToDecimal(LightMoneyUnit.BTC),
|
||||
StoreId = masterClients.Key,
|
||||
Destination = new LNURLPayClaimDestinaton(destination),
|
||||
PreApprove = true,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", LightningPaymentType.Instance),
|
||||
Metadata = JObject.FromObject(new
|
||||
{
|
||||
Source = $"MicroNode on store {storeId.Key}"
|
||||
})
|
||||
});
|
||||
if (payout.PayoutData is not null)
|
||||
{
|
||||
await UpsertRecord(client.Key, payout.PayoutData);
|
||||
}
|
||||
}, -1, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.WhenAny(Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)).ContinueWith(
|
||||
task =>
|
||||
{
|
||||
if (!task.IsCanceled)
|
||||
PushEvent(new CreatePayoutEvt());
|
||||
}, cancellationToken);
|
||||
|
||||
}else if (evt is StoreRemovedEvent storeRemovedEvent)
|
||||
{
|
||||
_storeSettings.Remove(storeRemovedEvent.StoreId);
|
||||
_ownerSettings.Remove(storeRemovedEvent.StoreId);
|
||||
}
|
||||
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, int> _retries = new ();
|
||||
private bool InvalidateAfterRetries(string id, int max)
|
||||
{
|
||||
return _retries.AddOrUpdate(id, 1, (_, i) => i + 1) >= max;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<PayoutEvent>();
|
||||
Subscribe<CreatePayoutEvt>();
|
||||
Subscribe<StoreRemovedEvent>();
|
||||
Subscribe<CheckActiveTransactions>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, string> _keyToMasterStoreId = new();
|
||||
|
||||
public async Task Set(string storeId, MicroNodeStoreSettings? settings, string? masterStoreId = null)
|
||||
{
|
||||
await _init.Task;
|
||||
if (settings is null)
|
||||
{
|
||||
_storeSettings.Remove(storeId);
|
||||
_keyToMasterStoreId.TryRemove(settings!.Key, out _);
|
||||
await _storeRepository.UpdateSetting(storeId, StoreSettingsKey, (MicroNodeStoreSettings?) null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_storeSettings[storeId] = settings;
|
||||
if (masterStoreId is not null)
|
||||
{
|
||||
_keyToMasterStoreId[settings.Key] = masterStoreId;
|
||||
}
|
||||
|
||||
await _storeRepository.UpdateSetting(storeId, StoreSettingsKey, settings);
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
await ctx.MicroAccounts.Upsert(new MicroAccount()
|
||||
{
|
||||
Balance = 0,
|
||||
BalanceCheckpoint = 0,
|
||||
MasterStoreId = masterStoreId,
|
||||
Key = settings.Key,
|
||||
}).NoUpdate().RunAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetMaster(string storeId, MicroNodeSettings? settings)
|
||||
{
|
||||
await _init.Task;
|
||||
if (settings is null)
|
||||
{
|
||||
_ownerSettings.Remove(storeId);
|
||||
await _storeRepository.UpdateSetting(storeId, MasterSettingsKey, (MicroNodeSettings?) null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ownerSettings[storeId] = settings;
|
||||
await _storeRepository.UpdateSetting(storeId, MasterSettingsKey, settings);
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MicroAccount[]> GetMasterLiabilities(string storeId, bool includeTxs)
|
||||
{
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var query = ctx.MicroAccounts.AsQueryable();
|
||||
if (includeTxs)
|
||||
{
|
||||
query = query.Include(account => account.Transactions);
|
||||
}
|
||||
|
||||
return await query.Where(account => account.MasterStoreId == storeId).ToArrayAsync();
|
||||
}
|
||||
public async Task<MicroTransaction[]> GetTransactions(string storeId)
|
||||
{
|
||||
if(!_storeSettings.TryGetValue(storeId, out var settings))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
return await ctx.MicroTransactions.Where(t => t.AccountId == settings!.Key).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
11
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeSettings.cs
Normal file
11
Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeSettings.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodeSettings
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public bool AdminOnly { get; set; }
|
||||
// public decimal FeeReserve { get; set; }
|
||||
// public decimal ServiceFee { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodeStartupTask : IStartupTask
|
||||
{
|
||||
private readonly MicroNodeContextFactory _microNodeContextFactory;
|
||||
private readonly ILogger<MicroNodeStartupTask> _logger;
|
||||
|
||||
public MicroNodeStartupTask(MicroNodeContextFactory microNodeContextFactory, ILogger<MicroNodeStartupTask> logger)
|
||||
{
|
||||
_microNodeContextFactory = microNodeContextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken = new())
|
||||
{
|
||||
_logger.LogInformation("Migrating MicroNode database");
|
||||
await using var ctx = _microNodeContextFactory.CreateContext();
|
||||
var pendingMigrations = await ctx.Database.GetPendingMigrationsAsync(cancellationToken: cancellationToken);
|
||||
if (pendingMigrations.Any())
|
||||
{
|
||||
_logger.LogInformation("Applying {Count} migrations", pendingMigrations.Count());
|
||||
await ctx.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("No migrations to apply");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroNodeStoreSettings
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string? ForwardDestination { get; set; }
|
||||
}
|
||||
21
Plugins/BTCPayServer.Plugins.MicroNode/MicroTransaction.cs
Normal file
21
Plugins/BTCPayServer.Plugins.MicroNode/MicroTransaction.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode;
|
||||
|
||||
public class MicroTransaction
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AccountId { get; set; }
|
||||
|
||||
public long Amount { get; set; }
|
||||
public bool Accounted { get; set; }
|
||||
|
||||
|
||||
public string Type { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public MicroAccount Account { get; set; }
|
||||
public string? DependentId { get; set; }
|
||||
public MicroTransaction? Dependent { get; set; }
|
||||
|
||||
public List<MicroTransaction> Dependents { get; set; }
|
||||
}
|
||||
130
Plugins/BTCPayServer.Plugins.MicroNode/Migrations/20240115112915_Initial.Designer.cs
generated
Normal file
130
Plugins/BTCPayServer.Plugins.MicroNode/Migrations/20240115112915_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,130 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Plugins.MicroNode;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode.Migrations
|
||||
{
|
||||
[DbContext(typeof(MicroNodeContext))]
|
||||
[Migration("20240115112915_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("BTCPayServer.Plugins.MicroNode")
|
||||
.HasAnnotation("ProductVersion", "8.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroAccount", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("BalanceCheckpoint")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("MasterStoreId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("MicroAccounts", "BTCPayServer.Plugins.MicroNode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Accounted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("Amount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DependentAccountId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DependentId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id", "AccountId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("DependentId", "AccountId");
|
||||
|
||||
b.HasIndex("DependentId", "DependentAccountId");
|
||||
|
||||
b.ToTable("MicroTransactions", "BTCPayServer.Plugins.MicroNode", t =>
|
||||
{
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION");
|
||||
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION");
|
||||
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION");
|
||||
});
|
||||
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\nRETURN OLD;\r\nEND;\r\n$LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION AFTER DELETE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"();")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF NEW.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\"\r\n WHERE NEW.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION AFTER INSERT\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"();")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$\r\nBEGIN\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\", \"DependentId\" = NEW.\"Id\"\r\n WHERE OLD.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS FALSE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS TRUE AND OLD.\"Amount\" <> NEW.\"Amount\" THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS FALSE AND OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION AFTER UPDATE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"();");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroAccount", "Account")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroTransaction", null)
|
||||
.WithMany("Dependents")
|
||||
.HasForeignKey("DependentId", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroTransaction", "Dependent")
|
||||
.WithMany()
|
||||
.HasForeignKey("DependentId", "DependentAccountId");
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Dependent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroAccount", b =>
|
||||
{
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.Navigation("Dependents");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "BTCPayServer.Plugins.MicroNode");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MicroAccounts",
|
||||
schema: "BTCPayServer.Plugins.MicroNode",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "text", nullable: false),
|
||||
Balance = table.Column<long>(type: "bigint", nullable: false),
|
||||
BalanceCheckpoint = table.Column<long>(type: "bigint", nullable: false),
|
||||
MasterStoreId = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MicroAccounts", x => x.Key);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MicroTransactions",
|
||||
schema: "BTCPayServer.Plugins.MicroNode",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
AccountId = table.Column<string>(type: "text", nullable: false),
|
||||
Amount = table.Column<long>(type: "bigint", nullable: false),
|
||||
Accounted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Type = table.Column<string>(type: "text", nullable: true),
|
||||
Active = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DependentId = table.Column<string>(type: "text", nullable: true),
|
||||
DependentAccountId = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MicroTransactions", x => new { x.Id, x.AccountId });
|
||||
table.ForeignKey(
|
||||
name: "FK_MicroTransactions_MicroAccounts_AccountId",
|
||||
column: x => x.AccountId,
|
||||
principalSchema: "BTCPayServer.Plugins.MicroNode",
|
||||
principalTable: "MicroAccounts",
|
||||
principalColumn: "Key",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MicroTransactions_MicroTransactions_DependentId_AccountId",
|
||||
columns: x => new { x.DependentId, x.AccountId },
|
||||
principalSchema: "BTCPayServer.Plugins.MicroNode",
|
||||
principalTable: "MicroTransactions",
|
||||
principalColumns: new[] { "Id", "AccountId" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MicroTransactions_MicroTransactions_DependentId_DependentAc~",
|
||||
columns: x => new { x.DependentId, x.DependentAccountId },
|
||||
principalSchema: "BTCPayServer.Plugins.MicroNode",
|
||||
principalTable: "MicroTransactions",
|
||||
principalColumns: new[] { "Id", "AccountId" });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MicroTransactions_AccountId",
|
||||
schema: "BTCPayServer.Plugins.MicroNode",
|
||||
table: "MicroTransactions",
|
||||
column: "AccountId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MicroTransactions_DependentId_AccountId",
|
||||
schema: "BTCPayServer.Plugins.MicroNode",
|
||||
table: "MicroTransactions",
|
||||
columns: new[] { "DependentId", "AccountId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MicroTransactions_DependentId_DependentAccountId",
|
||||
schema: "BTCPayServer.Plugins.MicroNode",
|
||||
table: "MicroTransactions",
|
||||
columns: new[] { "DependentId", "DependentAccountId" });
|
||||
|
||||
migrationBuilder.Sql("CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\nRETURN OLD;\r\nEND;\r\n$LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION AFTER DELETE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"();");
|
||||
|
||||
migrationBuilder.Sql("CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF NEW.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\"\r\n WHERE NEW.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION AFTER INSERT\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"();");
|
||||
|
||||
migrationBuilder.Sql("CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$\r\nBEGIN\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\", \"DependentId\" = NEW.\"Id\"\r\n WHERE OLD.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS FALSE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS TRUE AND OLD.\"Amount\" <> NEW.\"Amount\" THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS FALSE AND OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION AFTER UPDATE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"();");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("DROP FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"() CASCADE;");
|
||||
|
||||
migrationBuilder.Sql("DROP FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"() CASCADE;");
|
||||
|
||||
migrationBuilder.Sql("DROP FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"() CASCADE;");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MicroTransactions",
|
||||
schema: "BTCPayServer.Plugins.MicroNode");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MicroAccounts",
|
||||
schema: "BTCPayServer.Plugins.MicroNode");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Plugins.MicroNode;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Plugins.MicroNode.Migrations
|
||||
{
|
||||
[DbContext(typeof(MicroNodeContext))]
|
||||
partial class MicroNodeContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("BTCPayServer.Plugins.MicroNode")
|
||||
.HasAnnotation("ProductVersion", "8.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroAccount", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("BalanceCheckpoint")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("MasterStoreId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("MicroAccounts", "BTCPayServer.Plugins.MicroNode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Accounted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("Amount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DependentAccountId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DependentId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id", "AccountId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("DependentId", "AccountId");
|
||||
|
||||
b.HasIndex("DependentId", "DependentAccountId");
|
||||
|
||||
b.ToTable("MicroTransactions", "BTCPayServer.Plugins.MicroNode", t =>
|
||||
{
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION");
|
||||
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION");
|
||||
|
||||
t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION");
|
||||
});
|
||||
|
||||
b
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\nRETURN OLD;\r\nEND;\r\n$LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION AFTER DELETE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_DELETE_MICROTRANSACTION\"();")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$\r\nBEGIN\r\n \r\n IF NEW.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n END IF;\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = NEW.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\"\r\n WHERE NEW.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION AFTER INSERT\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_INSERT_MICROTRANSACTION\"();")
|
||||
.HasAnnotation("LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION", "CREATE FUNCTION \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$\r\nBEGIN\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"BalanceCheckpoint\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"BalanceCheckpoint\" + 1\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\n SET \"Accounted\" = NEW.\"Accounted\", \"Active\" = NEW.\"Active\", \"DependentId\" = NEW.\"Id\"\r\n WHERE OLD.\"Id\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"DependentId\" AND \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\".\"AccountId\" = NEW.\"AccountId\";\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS FALSE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS TRUE AND OLD.\"Accounted\" IS TRUE AND OLD.\"Amount\" <> NEW.\"Amount\" THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\" + NEW.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\n \r\n IF NEW.\"Accounted\" IS FALSE AND OLD.\"Accounted\" IS TRUE THEN \r\n UPDATE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\"\r\n SET \"Balance\" = \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Balance\" - OLD.\"Amount\"\r\n WHERE \"BTCPayServer.Plugins.MicroNode\".\"MicroAccounts\".\"Key\" = OLD.\"AccountId\";\r\n END IF;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION AFTER UPDATE\r\nON \"BTCPayServer.Plugins.MicroNode\".\"MicroTransactions\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"BTCPayServer.Plugins.MicroNode\".\"LC_TRIGGER_AFTER_UPDATE_MICROTRANSACTION\"();");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroAccount", "Account")
|
||||
.WithMany("Transactions")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroTransaction", null)
|
||||
.WithMany("Dependents")
|
||||
.HasForeignKey("DependentId", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Plugins.MicroNode.MicroTransaction", "Dependent")
|
||||
.WithMany()
|
||||
.HasForeignKey("DependentId", "DependentAccountId");
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Dependent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroAccount", b =>
|
||||
{
|
||||
b.Navigation("Transactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Plugins.MicroNode.MicroTransaction", b =>
|
||||
{
|
||||
b.Navigation("Dependents");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Plugins.MicroNode
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.MicroNode.MicroNodeStoreSettings?
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@inject MicroNodeService MicroNodeService
|
||||
@{
|
||||
ViewData.SetActivePage("MicroNode ", "Configure", "Configure");
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
var isAdmin = (await AuthorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
|
||||
var masters = (await MicroNodeService.GetMasterSettings())
|
||||
.Where(pair => pair.Key != storeId && (isAdmin || pair.Value.AdminOnly is false))
|
||||
.Select(m => new SelectListItem(m.Value.Name, m.Key)).ToArray();
|
||||
var masterId = Context.Items.TryGetValue("MasterId", out var masterIdObj) ? masterIdObj?.ToString() : null;
|
||||
|
||||
var payments = await MicroNodeService.GetTransactions(storeId);
|
||||
}
|
||||
<form method="post" asp-action="Configure" asp-controller="MicroNode" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
@if (masters.Any())
|
||||
{
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Save</button>
|
||||
}
|
||||
@if (Model?.Key is not null)
|
||||
{
|
||||
<button name="command" type="submit" value="clear" class="btn btn-danger">Clear</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (masters.Any())
|
||||
{
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="form-group">
|
||||
<label for="masterStoreId" class="form-label" data-required>Master</label>
|
||||
<select name="masterStoreId" id="masterStoreId" asp-items="@masters" class="form-select" value="@masterId"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ForwardDestination" class="form-label">Forward Destination</label>
|
||||
<input asp-for="ForwardDestination" class="form-control"/>
|
||||
<span asp-validation-for="ForwardDestination" class="text-danger"></span>
|
||||
<p class="text-muted pt-2">Forward Destination</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<p class="mb-0">There is no master node available to use</p>
|
||||
</div>
|
||||
}
|
||||
@if (payments?.Any() is true)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Accounted</th>
|
||||
<th>Active</th>
|
||||
<th>Type</th>
|
||||
<th>Amount</th>
|
||||
|
||||
</tr>
|
||||
@foreach (var p in payments)
|
||||
{
|
||||
<tr>
|
||||
<td>@p.Id</td>
|
||||
<td>@p.Accounted</td>
|
||||
<td>@p.Active</td>
|
||||
<td>@p.Type</td>
|
||||
<td>@LightMoney.MilliSatoshis(p.Amount).ToDecimal(LightMoneyUnit.BTC) BTC</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<input type="hidden" asp-for="Key"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,102 @@
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Lightning
|
||||
@using BTCPayServer.Plugins.MicroNode
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.MicroNode.MicroNodeSettings
|
||||
@inject MicroNodeService MicroNodeService
|
||||
@{
|
||||
ViewData.SetActivePage("MicroNode ", "Configure Master", "ConfigureMaster");
|
||||
var storeId = Context.GetCurrentStoreId();
|
||||
}
|
||||
<form method="post" asp-action="ConfigureMaster" asp-controller="MicroNode" asp-route-storeId="@storeId">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">
|
||||
<span>@ViewData["Title"]</span>
|
||||
</h3>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0">
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Save</button>
|
||||
@if (Model is not null)
|
||||
{
|
||||
<button name="command" type="submit" value="clear" class="btn btn-danger">Clear</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="form-group form-check">
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="Enabled" class="form-check-label"></label>
|
||||
<span asp-validation-for="Enabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input asp-for="AdminOnly" type="checkbox" class="form-check-input"/>
|
||||
<label asp-for="AdminOnly" class="form-check-label"></label>
|
||||
<span asp-validation-for="AdminOnly" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label" ></label>
|
||||
<input asp-for="Name" type="text" class="form-control" required/>
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model is not null)
|
||||
{
|
||||
var ls = await MicroNodeService.GetMasterLiabilities(storeId, true);
|
||||
|
||||
<table class="table">
|
||||
|
||||
<tr>
|
||||
<th>Store id</th>
|
||||
<th>Key</th>
|
||||
<th>Balance</th>
|
||||
|
||||
</tr>
|
||||
|
||||
@foreach (var l in ls)
|
||||
{
|
||||
var ssk = await MicroNodeService.GetStoreSettingsFromKey(l.Key);
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
@ssk?.Key
|
||||
</td>
|
||||
<td>@l.Key</td>
|
||||
<td>@LightMoney.MilliSatoshis(l.Balance).ToDecimal(LightMoneyUnit.BTC) BTC</td>
|
||||
</tr>
|
||||
<tr >
|
||||
<td colspan="2">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Accounted</th>
|
||||
<th>Active</th>
|
||||
<th>Type</th>
|
||||
<th>Amount</th>
|
||||
|
||||
</tr>
|
||||
@foreach(var p in l.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@p.Id</td>
|
||||
<td>@p.Accounted</td>
|
||||
<td>@p.Active</td>
|
||||
<td>@p.Type</td>
|
||||
<td>@LightMoney.MilliSatoshis(p.Amount).ToDecimal(LightMoneyUnit.BTC) BTC</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,66 @@
|
||||
@inject MicroNodeService MicroNodeService;
|
||||
@using BTCPayServer.Plugins.MicroNode
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||
@{
|
||||
var storeId = Model.StoreId;
|
||||
if (Model.CryptoCode != "BTC")
|
||||
{
|
||||
return;
|
||||
}
|
||||
var client = await MicroNodeService.GetStoreSettings(storeId);
|
||||
}
|
||||
|
||||
<div id="MicroNodeSetup" class="pt-3 tab-pane fade" role="tabpanel" aria-labelledby="LightningNodeType-MicroNode">
|
||||
@if (client is not null)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-action="Configure" asp-controller="MicroNode" asp-route-storeId="@storeId">MicroNode needs to be configured beforehand.</a>
|
||||
}
|
||||
</div>
|
||||
@if (client is not null)
|
||||
{
|
||||
<script>
|
||||
const typePrefix = 'type=micro;key=@client.Key';
|
||||
const triggerEl = document.getElementById('LightningNodeType-MicroNode')
|
||||
const connStringEl = document.getElementById('ConnectionString')
|
||||
const connString = connStringEl.value;
|
||||
const isMicro = connString.startsWith(typePrefix);
|
||||
|
||||
|
||||
if (isMicro) {
|
||||
|
||||
// deactivate currently active tab and activate Breez tab
|
||||
const activeEl = document.querySelector('input[name="LightningNodeType"]:checked')
|
||||
if (activeEl) {
|
||||
activeEl.removeAttribute('checked')
|
||||
activeEl.setAttribute('aria-selected', 'false')
|
||||
document.querySelector('#LightningNodeTypeTabs .tab-pane.active').classList.remove('active', 'show')
|
||||
triggerEl.setAttribute('checked', 'checked')
|
||||
triggerEl.setAttribute('aria-selected', 'true')
|
||||
document.getElementById('MicroNodeSetup').classList.add('active', 'show')
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (isBreez) {
|
||||
const tabTrigger = new bootstrap.Tab(triggerEl)
|
||||
triggerEl.checked = true
|
||||
tabTrigger.show()
|
||||
}
|
||||
|
||||
delegate('change', 'input[name="LightningNodeType"]', e => {
|
||||
|
||||
const activeEl = document.querySelector('input[name="LightningNodeType"]:checked')
|
||||
if (activeEl.id === "LightningNodeType-MicroNode"){
|
||||
connStringEl.value =typePrefix;
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@inject MicroNodeService MicroNodeService;
|
||||
@using BTCPayServer.Plugins.MicroNode
|
||||
@model BTCPayServer.Models.StoreViewModels.LightningNodeViewModel
|
||||
|
||||
@if (Model.CryptoCode != "BTC")
|
||||
{
|
||||
return;
|
||||
}
|
||||
@{
|
||||
var client = await MicroNodeService.GetStoreSettings(Model.StoreId);
|
||||
var storeId = Model.StoreId;
|
||||
}
|
||||
@if (client is null)
|
||||
{
|
||||
<a asp-action="Configure" asp-controller="MicroNode" asp-route-storeId="@storeId" value="Custom" type="radio" id="LightningNodeType-MicroNode" role="tab" aria-controls="MicroNodeSetup" aria-selected="false" name="LightningNodeType" ><label for="LightningNodeType-MicroNode">Configure Micro Node</label></a>
|
||||
|
||||
}else{
|
||||
<input value="Custom" type="radio" id="LightningNodeType-MicroNode" data-bs-toggle="pill" data-bs-target="#MicroNodeSetup" role="tab" aria-controls="MicroNodeSetup" aria-selected="false" name="LightningNodeType">
|
||||
<label for="LightningNodeType-MicroNode">Use Micro node</label>}
|
||||
@@ -0,0 +1,31 @@
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Plugins.MicroNode
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@inject MicroNodeService MicroNodeService
|
||||
@{
|
||||
var storeId = ScopeProvider.GetCurrentStoreId();
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
var masters = (await MicroNodeService.GetMasterSettings()).Select(m => new SelectListItem(m.Value.Name, m.Key)).ToArray();
|
||||
@if (masters.Any())
|
||||
{
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a permission="@Policies.CanModifyStoreSettings" asp-controller="MicroNode" asp-action="Configure" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("MicroNode")" id="Nav-MicroNode">
|
||||
<span>MicroNode</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
}
|
||||
|
||||
<li class="nav-item">
|
||||
<a permission="@Policies.CanModifyStoreSettings" asp-controller="MicroNode" asp-action="ConfigureMaster" asp-route-storeId="@storeId" class="nav-link @ViewData.IsActivePage("MicroNodeMaster")" id="Nav-MicroNodeMaster">
|
||||
<span>MicroNode Master</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@inject BTCPayServer.Abstractions.Services.Safe Safe
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
@addTagHelper *, BTCPayServer.TagHelpers
|
||||
@addTagHelper *, BTCPayServer.Views.TagHelpers
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
Reference in New Issue
Block a user