Automatically set default payment methods for the store
Triggered on - store select/change - Onchain wallet becoming availble - Lightning node becoming availble Conditions: - User needs to be permissioned to manage the store - App onchain wallet is available and the store doesn't have an onchain payment method configured - App Lightning node is available and the store doesn't have a Lightning node configured Closes #43. Improve wallet settings
This commit is contained in:
parent
85643b8b2e
commit
882c22bfd3
@ -9,6 +9,8 @@ namespace BTCPayApp.Core.Attempt2;
|
||||
|
||||
public class LightningNodeManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-LN";
|
||||
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly ILogger<LightningNodeManager> _logger;
|
||||
private readonly OnChainWalletManager _onChainWalletManager;
|
||||
@ -22,6 +24,9 @@ public class LightningNodeManager : BaseHostedService
|
||||
private bool IsOnchainConfigured => _onChainWalletManager.WalletConfig is not null;
|
||||
private bool IsOnchainLightningDerivationConfigured => _onChainWalletManager.WalletConfig?.Derivations.ContainsKey(WalletDerivation.LightningScripts) is true;
|
||||
public bool CanConfigureLightningNode => IsHubConnected && IsOnchainConfigured && !IsOnchainLightningDerivationConfigured && State == LightningNodeState.NotConfigured;
|
||||
public string? ConnectionString => IsOnchainLightningDerivationConfigured
|
||||
? $"type=app;group={_onChainWalletManager.WalletConfig!.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower()
|
||||
: null;
|
||||
|
||||
public LightningNodeState State
|
||||
{
|
||||
|
||||
@ -14,6 +14,8 @@ using TxOut = NBitcoin.TxOut;
|
||||
namespace BTCPayApp.Core.Attempt2;
|
||||
public class OnChainWalletManager : BaseHostedService
|
||||
{
|
||||
public const string PaymentMethodId = "BTC-CHAIN";
|
||||
|
||||
private readonly IConfigProvider _configProvider;
|
||||
private readonly BTCPayAppServerClient _btcPayAppServerClient;
|
||||
private readonly BTCPayConnectionManager _btcPayConnectionManager;
|
||||
@ -75,7 +77,7 @@ public class OnChainWalletManager : BaseHostedService
|
||||
}
|
||||
|
||||
private bool IsHubConnected => _btcPayConnectionManager.ConnectionState is HubConnectionState.Connected;
|
||||
private bool IsConfigured => WalletConfig is not null;
|
||||
public bool IsConfigured => WalletConfig is not null;
|
||||
|
||||
private async Task OnStateChanged(object? sender, (OnChainWalletState Old, OnChainWalletState New) e)
|
||||
{
|
||||
|
||||
@ -4,7 +4,6 @@ using BTCPayApp.Core.AspNetRip;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Helpers;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@ -34,6 +33,8 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
|
||||
public AuthStateProvider(
|
||||
IConfigProvider config,
|
||||
@ -145,7 +146,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
public async Task<bool> IsAuthorized(string policy, object? resource = null)
|
||||
{
|
||||
var authState = await GetAuthenticationStateAsync();
|
||||
var result = await _authService.AuthorizeAsync(authState.User, resource,Policies.CanViewStoreSettings);
|
||||
var result = await _authService.AuthorizeAsync(authState.User, resource, policy);
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
@ -162,6 +163,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
var store = GetUserStore(storeId);
|
||||
if (store == null) return new FormResult(false, $"Store with ID '{storeId}' does not exist or belong to the user.");
|
||||
|
||||
OnBeforeStoreChange?.Invoke(this, GetCurrentStore());
|
||||
string? message = null;
|
||||
|
||||
// create associated POS app if there is none
|
||||
@ -183,6 +185,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAccountManager, I
|
||||
|
||||
_account!.CurrentStoreId = storeId;
|
||||
await UpdateAccount(_account);
|
||||
OnAfterStoreChange?.Invoke(this, store);
|
||||
|
||||
return new FormResult(true, string.IsNullOrEmpty(message) ? null : [message]);
|
||||
}
|
||||
|
||||
@ -29,4 +29,6 @@ public interface IAccountManager
|
||||
public Task RemoveAccount(BTCPayAccount account);
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnBeforeAccountChange { get; set; }
|
||||
public AsyncEventHandler<BTCPayAccount?>? OnAfterAccountChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnBeforeStoreChange { get; set; }
|
||||
public AsyncEventHandler<AppUserStoreInfo?>? OnAfterStoreChange { get; set; }
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
@inject OnChainWalletManager OnChainWalletManager
|
||||
@inject LightningNodeManager LightningNodeManager
|
||||
@inject IState<RootState> State
|
||||
@inject ILogger<IndexPage> Logger
|
||||
@ -82,7 +81,7 @@
|
||||
private LDKNode? Node => LightningNodeManager.Node;
|
||||
|
||||
private string? ConfiguredConnectionString;
|
||||
private string ConnectionString => $"type=app;group={OnChainWalletManager.WalletConfig?.Derivations[WalletDerivation.LightningScripts].Identifier}".ToLower();
|
||||
private string ConnectionString => LightningNodeManager.ConnectionString;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@ -124,7 +123,7 @@
|
||||
var acc = AccountManager.GetAccount();
|
||||
if (acc?.CurrentStoreId != null)
|
||||
{
|
||||
var pm = await AccountManager.GetClient().UpdateStorePaymentMethod(acc.CurrentStoreId, "BTC-LN", new UpdatePaymentMethodRequest()
|
||||
var pm = await AccountManager.GetClient().UpdateStorePaymentMethod(acc.CurrentStoreId, "BTC-LN", new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = new JObject
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
@attribute [Route(Routes.WalletSettings)]
|
||||
@layout BaseLayout
|
||||
@using BTCPayApp.CommonServer.Models
|
||||
@using BTCPayApp.UI.Features
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using BTCPayApp.Core.Attempt2
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@using BTCPayApp.Core.Helpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
@ -14,22 +14,18 @@
|
||||
@inject IState<RootState> State
|
||||
@inject ILogger<IndexPage> Logger
|
||||
@inject IAccountManager AccountManager
|
||||
<PageTitle>Wallet Setup</PageTitle>
|
||||
<PageTitle>Onchain Wallet</PageTitle>
|
||||
|
||||
<SectionContent SectionId="_Layout.Top">
|
||||
<Titlebar Back>
|
||||
<h1>Wallet Setup</h1>
|
||||
<h1>Onchain Wallet</h1>
|
||||
</Titlebar>
|
||||
</SectionContent>
|
||||
<section class="container">
|
||||
@if (CanConfigureWallet)
|
||||
@if (OnChainWalletManager.IsConfigured)
|
||||
{
|
||||
<button class="btn btn-primary" @onclick="GenerateWallet">Generate Wallet</button>
|
||||
}
|
||||
@if (OnChainWalletManager.State == OnChainWalletState.Loaded)
|
||||
{
|
||||
var config = OnChainWalletManager.WalletConfig;
|
||||
<h3>@config.Network Wallet</h3>
|
||||
var config = OnChainWalletManager.WalletConfig!;
|
||||
<h3>Network: @config.Network</h3>
|
||||
<p>Fingerprint: <code>@config.Fingerprint</code></p>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#seed" aria-expanded="false" aria-controls="seed">
|
||||
Show seed
|
||||
@ -48,66 +44,49 @@
|
||||
<h3 class="mt-4">Derivations</h3>
|
||||
@foreach (var d in config.Derivations)
|
||||
{
|
||||
var identifier = d.Value.Identifier;
|
||||
var descriptor = d.Value.Descriptor;
|
||||
var isStorePaymentMethod = IsStorePaymentMethodId(identifier);
|
||||
<div class="box mb-3">
|
||||
<h4>@(d.Value.Name)</h4>
|
||||
|
||||
@if (!string.IsNullOrEmpty(d.Value.Identifier))
|
||||
@if (!string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@d.Value.Identifier" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext" />
|
||||
<TruncateCenter Text="@identifier" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext" />
|
||||
<label>Identifier</label>
|
||||
</div>
|
||||
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(d.Value.Descriptor))
|
||||
@if (!string.IsNullOrEmpty(descriptor))
|
||||
{
|
||||
<div class="form-floating">
|
||||
<TruncateCenter Text="@d.Value.Descriptor" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext" />
|
||||
<TruncateCenter Text="@descriptor" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext" />
|
||||
<label>Descriptor</label>
|
||||
</div>
|
||||
}
|
||||
@if (d.Value.Descriptor is not null && d.Value.Identifier is not null && (StorePaymentMethodIdentifier is null || !d.Value.Identifier.Contains(StorePaymentMethodIdentifier)))
|
||||
{
|
||||
<button class="btn btn-primary" @onclick="() => SetStorePaymentMethod(d.Key)">Set as Store Payment Method</button>
|
||||
@if (isStorePaymentMethod is false)
|
||||
{
|
||||
<button class="btn btn-primary" @onclick="() => SetStorePaymentMethod(d.Key)">
|
||||
Set as onchain payment method for store "@Store!.Name"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul>
|
||||
<li>Connection state: @State.Value.ConnectionState</li>
|
||||
<li>Onchain wallet state: @State.Value.OnchainWalletState</li>
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private bool CanConfigureWallet =>
|
||||
State.Value.OnchainWalletState == OnChainWalletState.NotConfigured &&
|
||||
State.Value.ConnectionState == HubConnectionState.Connected;
|
||||
|
||||
private string? StorePaymentMethodIdentifier = null;
|
||||
|
||||
private async Task SetStorePaymentMethod(string key)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
if (!OnChainWalletManager.WalletConfig.Derivations.TryGetValue(key, out var derivation) || derivation.Descriptor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xpub = derivation.Descriptor.ExtractFromDescriptor(OnChainWalletManager.Network);
|
||||
var identifierSUffix = derivation.Identifier.Split(':').Last();
|
||||
var acc = AccountManager.GetAccount();
|
||||
if (acc?.CurrentStoreId is null)
|
||||
return;
|
||||
var pm = await AccountManager.GetClient().UpdateStorePaymentMethod(acc.CurrentStoreId, "BTC-CHAIN", new UpdatePaymentMethodRequest()
|
||||
{
|
||||
Enabled = true,
|
||||
Config = derivation.Descriptor
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error setting store payment method");
|
||||
}
|
||||
}
|
||||
private AppUserStoreInfo? Store => AccountManager.GetCurrentStore();
|
||||
private string? _storePaymentMethodIdentifier;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@ -118,32 +97,46 @@
|
||||
|
||||
private async Task GetStorePaymentMethod()
|
||||
{
|
||||
var storeId = AccountManager.GetCurrentStore()?.Id;
|
||||
var storeId = Store?.Id;
|
||||
var pmId = OnChainWalletManager.PaymentMethodId;
|
||||
if (string.IsNullOrEmpty(storeId) || CanConfigureWallet) return;
|
||||
try
|
||||
{
|
||||
var pm = await AccountManager.GetClient().GetStorePaymentMethod(storeId, "BTC-CHAIN", true);
|
||||
var pm = await AccountManager.GetClient().GetStorePaymentMethod(storeId, pmId, true);
|
||||
if (pm?.Config is JObject configObj && configObj.TryGetValue("accountDerivation", out var derivationSchemeToken) && derivationSchemeToken.Value<string>() is {} derivationScheme)
|
||||
{
|
||||
StorePaymentMethodIdentifier = derivationScheme;
|
||||
_storePaymentMethodIdentifier = derivationScheme;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Payment method BTC-CHAIN unset for store {StoreId}", storeId);
|
||||
Logger.LogDebug(ex, "Payment method {PaymentMethodId} unset for store {StoreId}", pmId, storeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task GenerateWallet()
|
||||
private async Task SetStorePaymentMethod(string key)
|
||||
{
|
||||
var pmId = OnChainWalletManager.PaymentMethodId;
|
||||
var storeId = AccountManager.GetCurrentStore()?.Id;
|
||||
try
|
||||
{
|
||||
await OnChainWalletManager.Generate();
|
||||
if (storeId is null || OnChainWalletManager.WalletConfig?.Derivations.TryGetValue(key, out var derivation) is not true || derivation.Descriptor is null) return;
|
||||
await AccountManager.GetClient().UpdateStorePaymentMethod(storeId, pmId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = derivation.Descriptor
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error configuring wallet");
|
||||
Logger.LogError(ex, "Error setting payment method {PaymentMethodId} for store {StoreId}", pmId, storeId);
|
||||
}
|
||||
}
|
||||
|
||||
private bool? IsStorePaymentMethodId(string identifier)
|
||||
{
|
||||
return !string.IsNullOrEmpty(identifier) && !string.IsNullOrEmpty(_storePaymentMethodIdentifier)
|
||||
? identifier.Contains(_storePaymentMethodIdentifier)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,12 +25,6 @@
|
||||
</Titlebar>
|
||||
</SectionContent>
|
||||
<section class="container">
|
||||
<ul>
|
||||
<li>Connection state: @State.Value.ConnectionState</li>
|
||||
<li>Onchain wallet state: @State.Value.OnchainWalletState</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
using BTCPayApp.Core.Attempt2;
|
||||
using BTCPayApp.Core.Auth;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Data;
|
||||
using BTCPayApp.UI.Features;
|
||||
using Fluxor;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayApp.UI;
|
||||
|
||||
@ -54,16 +58,24 @@ public class StateMiddleware(
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
lightningNodeService.StateChanged += (sender, args) =>
|
||||
{
|
||||
dispatcher.Dispatch(new RootState.LightningNodeStateUpdatedAction(lightningNodeService.State));
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
onChainWalletManager.StateChanged += (sender, args) =>
|
||||
onChainWalletManager.StateChanged += async (sender, args) =>
|
||||
{
|
||||
dispatcher.Dispatch(new RootState.OnChainWalletStateUpdatedAction(onChainWalletManager.State));
|
||||
return Task.CompletedTask;
|
||||
if (onChainWalletManager.State == OnChainWalletState.Loaded)
|
||||
await TryApplyingAppPaymentMethodsToCurrentStore(true, false);
|
||||
};
|
||||
|
||||
lightningNodeService.StateChanged += async (sender, args) =>
|
||||
{
|
||||
dispatcher.Dispatch(new RootState.LightningNodeStateUpdatedAction(lightningNodeService.State));
|
||||
|
||||
if (lightningNodeService.State == LightningNodeState.Loaded)
|
||||
await TryApplyingAppPaymentMethodsToCurrentStore(false, true);
|
||||
};
|
||||
|
||||
accountManager.OnAfterStoreChange += async (sender, storeInfo) =>
|
||||
{
|
||||
await TryApplyingAppPaymentMethodsToCurrentStore(true, true);
|
||||
};
|
||||
|
||||
btcpayAppServerClient.OnNotifyServerEvent += async (sender, serverEvent) =>
|
||||
@ -105,4 +117,36 @@ public class StateMiddleware(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(GenericPaymentMethodData? onchain, GenericPaymentMethodData? lightning)?> TryApplyingAppPaymentMethodsToCurrentStore(bool applyOnchain, bool applyLighting)
|
||||
{
|
||||
var storeId = accountManager.GetCurrentStore()?.Id;
|
||||
if (// is a store present?
|
||||
string.IsNullOrEmpty(storeId) ||
|
||||
// is user permitted? (store owner)
|
||||
!await accountManager.IsAuthorized(Policies.CanModifyStoreSettings, storeId) ||
|
||||
// is the onchain wallet configured?
|
||||
onChainWalletManager.WalletConfig?.Derivations.TryGetValue(WalletDerivation.NativeSegwit, out var onchainDerivation) is not true || string.IsNullOrEmpty(onchainDerivation.Descriptor)) return null;
|
||||
// check the store's payment methods
|
||||
var pms = await accountManager.GetClient().GetStorePaymentMethods(storeId);
|
||||
var onchain = pms.FirstOrDefault(pm => pm.PaymentMethodId == OnChainWalletManager.PaymentMethodId);
|
||||
if (onchain is null && applyOnchain)
|
||||
{
|
||||
onchain = await accountManager.GetClient().UpdateStorePaymentMethod(storeId, OnChainWalletManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = onchainDerivation.Descriptor
|
||||
});
|
||||
}
|
||||
var lightning = pms.FirstOrDefault(pm => pm.PaymentMethodId == LightningNodeManager.PaymentMethodId);
|
||||
if (lightning is null && !string.IsNullOrEmpty(lightningNodeService.ConnectionString) && applyLighting)
|
||||
{
|
||||
lightning = await accountManager.GetClient().UpdateStorePaymentMethod(storeId, LightningNodeManager.PaymentMethodId, new UpdatePaymentMethodRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Config = new JObject { ["connectionString"] = lightningNodeService.ConnectionString }
|
||||
});
|
||||
}
|
||||
return (onchain, lightning);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user