Skip to content

Commit

Permalink
Send: Add transactions list and onchain verify dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisreimann committed Dec 4, 2024
1 parent 057ef98 commit 42d0b66
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 101 deletions.
11 changes: 5 additions & 6 deletions BTCPayApp.Core/Helpers/StoreHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ public static async Task<bool> IsOnChainOurs(this OnChainWalletManager onChainWa
using var jsonDoc = JsonDocument.Parse(onchain.Config.ToString());
if (jsonDoc.RootElement.TryGetProperty("accountDerivation", out var derivationSchemeElement) &&
derivationSchemeElement.GetString() is { } derivationScheme &&
config.Derivations.Any(pair => pair.Value.Identifier ==
$"DERIVATIONSCHEME:{derivationScheme}"))
config.Derivations.Any(pair => pair.Value.Identifier == $"DERIVATIONSCHEME:{derivationScheme}"))
{
return true;
}
Expand All @@ -84,14 +83,14 @@ public static async Task<bool> IsLightningOurs(this LightningNodeManager lightni
{
if (!string.IsNullOrEmpty(lightning?.Config.ToString()))
{
var node = lightningNodeManager.Node;
if (node == null) return false;
using var jsonDoc = JsonDocument.Parse(lightning.Config.ToString());
if (jsonDoc.RootElement.TryGetProperty("connectionString", out var connectionStringElement) &&
connectionStringElement.GetString() is { } connectionString &&
LightningConnectionStringHelper.ExtractValues(connectionString, out var lnConnectionString) is { } lnValues &&
lnConnectionString == "app" &&
lnValues.TryGetValue("key", out var key) &&
key is not null &&
await lightningNodeManager.Node.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
lnConnectionString == "app" && lnValues.TryGetValue("key", out var key) && key is not null &&
await node.ApiKeyManager.CheckPermission(key, APIKeyPermission.Read))
{
return true;
}
Expand Down
17 changes: 11 additions & 6 deletions BTCPayApp.Core/LDK/PaymentsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public PaymentsManager(
_ldkNode = ldkNode;
}

public async Task<List<AppLightningPayment>> GetTransactions(CancellationToken cancellationToken = default)
{
return await List(payments => payments, cancellationToken);
}

public async Task<List<AppLightningPayment>> List(
Func<IQueryable<AppLightningPayment>, IQueryable<AppLightningPayment?>> filter,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -203,10 +208,10 @@ public async Task<AppLightningPayment> PayInvoice(BOLT11PaymentRequest paymentRe
await context.SaveChangesAsync();
if (successSelfPay)
{

OnPaymentUpdate?.Invoke(this, inbound);
}

OnPaymentUpdate?.Invoke(this, newOutbound);
return newOutbound;
}
Expand Down Expand Up @@ -250,7 +255,7 @@ public async Task<AppLightningPayment> PayInvoice(BOLT11PaymentRequest paymentRe
}
catch (Exception e)
{

outbound.Status = LightningPaymentStatus.Failed;
await context.SaveChangesAsync();
throw;
Expand Down Expand Up @@ -383,7 +388,7 @@ public async Task Handle(Event.Event_PaymentClaimed eventPaymentClaimed)
{
return;
}

var paymentHash = uint256.Parse(Convert.ToHexString(eventPaymentClaimed.payment_hash).ToLower());
await PaymentUpdate(paymentHash, true, "default", false,
preimage is null ? null : Convert.ToHexString(preimage).ToLower());
Expand All @@ -397,7 +402,7 @@ await PaymentUpdate(new uint256(eventPaymentFailed.payment_hash), false,

public async Task Handle(Event.Event_PaymentSent eventPaymentSent)
{

var paymentHash = uint256.Parse(Convert.ToHexString(eventPaymentSent.payment_hash).ToLower());
await PaymentUpdate(paymentHash, false,
Convert.ToHexString(
Expand All @@ -407,4 +412,4 @@ await PaymentUpdate(paymentHash, false,
}


}
}
20 changes: 9 additions & 11 deletions BTCPayApp.Core/Wallet/OnChainWalletManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using BTCPayApp.Core.Backup;
using BTCPayApp.Core.Backup;
using BTCPayApp.Core.BTCPayServer;
using BTCPayApp.Core.Contracts;
using BTCPayApp.Core.Data;
Expand Down Expand Up @@ -542,14 +542,14 @@ public async Task<IEnumerable<ICoin>> GetUTXOS()
}


public async Task<(NBitcoin.Transaction Tx, ICoin[] SpentCoins, BitcoinAddress Change)> CreateTransaction(
List<TxOut> txOuts, FeeRate? feeRate, List<Coin> explicitIns = null)
public async Task<(Transaction Tx, ICoin[] SpentCoins, BitcoinAddress Change)> CreateTransaction(
List<TxOut> txOuts, FeeRate? feeRate, List<Coin>? explicitIns = null)
{
var availableCoins = (await GetUTXOS()).OfType<CoinWithKey>().ToList();
feeRate ??= await GetFeeRate(1);

var config = await GetConfig();
//TODO: do not hardcode this constant
//TODO: do not hardcode this constant
var changeScript = await DeriveScript(WalletDerivation.NativeSegwit);
var txBuilder = config.NBitcoinNetwork
.CreateTransactionBuilder()
Expand All @@ -559,7 +559,6 @@ public async Task<IEnumerable<ICoin>> GetUTXOS()
txBuilder = txOuts.Aggregate(txBuilder, (current, c) => current.Send(c.ScriptPubKey, c.Value));
txBuilder.SendAllRemainingToChange();

NBitcoin.Transaction? tx;
if (explicitIns?.Any() is true)
{
txBuilder.AddCoins(explicitIns.ToArray());
Expand All @@ -569,15 +568,14 @@ public async Task<IEnumerable<ICoin>> GetUTXOS()
{
try
{
tx = txBuilder.BuildTransaction(true);
var tx = txBuilder.BuildTransaction(true);
return (tx, txBuilder.FindSpentCoins(tx), changeScript);
}
catch (NotEnoughFundsException e)
catch (NotEnoughFundsException)
{
if (!availableCoins.Any())
throw;
if (!availableCoins.Any()) throw;
var newCoin = availableCoins.First();
//TODO: switch to nuilding a psbt and signing with the ISignableCoin interface
//TODO: switch to building a PSBT and signing with the ISignableCoin interface
if (newCoin is CoinWithKey newCoinWithKey)
{
txBuilder.AddCoins(newCoin);
Expand All @@ -596,7 +594,7 @@ public async Task RemoveDerivation(params string[] key)
var config = await GetConfig();
if (State != OnChainWalletState.Loaded || config is null)
{
throw new InvalidOperationException("Cannot remove deriv in current state");
throw new InvalidOperationException("Cannot remove derivation in current state");
}

var updated = key.Aggregate(false, (current, k) => current || config.Derivations.Remove(k));
Expand Down
6 changes: 4 additions & 2 deletions BTCPayApp.UI/Components/LightningPaymentItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
<span class="text-muted">
<DateDisplay DateTimeOffset="@Payment.Timestamp"/>
</span>
<LightningPaymentStatusDisplay Payment="@Payment" OnCancelClick="OnCancelClick" />
<span class="w-100px ms-auto text-end">
<LightningPaymentStatusDisplay Payment="@Payment" OnCancelClick="OnCancelClick" />
</span>
</div>
<div class="collapse @(_showDetails ? "show" : null)">
<div class="py-3">
<div class="pt-3">
<div class="form-floating">
<TruncateCenter Text="@Payment.PaymentRequest.ToString()" Padding="15" Copy="true" class="form-control-plaintext"/>
<label>Payment Request</label>
Expand Down
10 changes: 0 additions & 10 deletions BTCPayApp.UI/Components/LightningPaymentItem.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,3 @@ a {
display: block;
color: var(--btcpay-body-text);
}
.invoice .badge-container {
flex: 0 0 5.125rem;
text-align: right;
}

@media (max-width: 359px) {
.invoice .badge-container {
flex-grow: 1;
}
}
53 changes: 53 additions & 0 deletions BTCPayApp.UI/Components/TransactionsList.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@using BTCPayApp.UI.Models
<div @attributes="InputAttributes" class="@CssClass">
@if (Loading)
{
<div class="p-3 text-center">
<LoadingIndicator/>
</div>
}
@if (Transactions is not null)
{
@if (Transactions.Any())
{
@foreach (var t in Transactions)
{
<div class="box">
<TransactionsListItem Transaction="@t" Unit="@Unit" OnToggleDisplayCurrency="OnToggleDisplayCurrency"/>
</div>
}
}
else if (!string.IsNullOrEmpty(Error))
{
<Alert Type="danger">@Error</Alert>
}
else
{
<div class="box">
<p class="text-muted my-0">There are no transactions, yet.</p>
</div>
}
}
</div>

@code {
[Parameter]
public IEnumerable<TransactionModel>? Transactions { get; set; }

[Parameter, EditorRequired]
public string? Unit { get; set; }

[Parameter]
public EventCallback OnToggleDisplayCurrency { get; set; }

[Parameter]
public bool Loading { get; set; }

[Parameter]
public string? Error { get; set; }

[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? InputAttributes { get; set; }

private string CssClass => $"transactions-list {(InputAttributes?.ContainsKey("class") is true ? InputAttributes["class"] : "")}".Trim();
}
7 changes: 7 additions & 0 deletions BTCPayApp.UI/Components/TransactionsList.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.transactions-list .box {
margin-bottom: 0 !important;
}

.transactions-list .box + .box {
margin-top: var(--btcpay-space-xs);
}
93 changes: 93 additions & 0 deletions BTCPayApp.UI/Components/TransactionsListItem.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@using BTCPayApp.UI.Models
@using BTCPayServer.Lightning

<div>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2" @onclick="() => _showDetails = !_showDetails">
<span class="flex-grow-1 text-muted">
<DateDisplay DateTimeOffset="@Transaction.Timestamp"/>
</span>
<AmountDisplay Value="Transaction.Value.ToDecimal(UnitLightMoney)" Unit="@Unit" OnToggleDisplayCurrency="ToggleDisplayCurrency" class="@AmountClass" />
<span class="w-100px ms-auto text-end">
<span class="@BadgeClass">@UnifyStatus(Transaction.Status)</span>
</span>
</div>
<div class="collapse @(_showDetails ? "show" : null)">
<div class="pt-3">
<div class="form-floating">
<span class="truncate-center form-control-plaintext" style="padding-right:3px;padding-left:3px;padding-bottom:0;font-weight: var(--btcpay-font-weight-semibold);">
<DateDisplay DateTimeOffset="@Transaction.Timestamp" Format="DateDisplay.DateDisplayFormat.Localized"/>
</span>
<label>Date</label>
</div>
@if (Transaction is { PaymentMethod: TransactionPaymentMethod.Onchain, OnchainTransaction: not null })
{
<div class="form-floating">
<TruncateCenter Text="@Transaction.OnchainTransaction.TransactionId" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
<label>Transaction ID</label>
</div>
}
else if (Transaction is { PaymentMethod: TransactionPaymentMethod.Lightning, LightningPayment: not null })
{
<div class="form-floating">
<TruncateCenter Text="@Transaction.LightningPayment.PaymentRequest.ToString()" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
<label>Payment Request</label>
</div>
@if (Transaction.LightningPayment.Preimage != null)
{
<div class="form-floating">
<TruncateCenter Text="@Transaction.LightningPayment.Preimage" Padding="15" Copy="true" Elastic="true" class="form-control-plaintext"/>
<label>Preimage</label>
</div>
}
}
</div>
</div>
</div>

@code {
[Parameter, EditorRequired]
public TransactionModel Transaction { get; set; } = null!;

[Parameter, EditorRequired]
public string? Unit { get; set; }

[Parameter]
public EventCallback OnToggleDisplayCurrency { get; set; }

private bool _showDetails;
private LightMoneyUnit UnitLightMoney => Unit == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
private string AmountClass => $"flex-grow-1 text-end fw-semibold text-{(Transaction.Type == TransactionType.Receive ? "success" : "danger")}";
private string BadgeClass
{
get
{
var clss = "badge";
var status = UnifyStatus(Transaction.Status).ToLower();
switch (Transaction.PaymentMethod)
{
case TransactionPaymentMethod.Onchain:
clss += $" badge-{(status == "unconfirmed" ? "pending" : "settled")}";
break;
case TransactionPaymentMethod.Lightning:
clss += $" badge-{status}";
break;
}
return clss;
}
}

private string UnifyStatus(string original)
{
return original switch
{
"Confirmed" or "Complete" => "Settled",
_ => original
};
}

private async Task ToggleDisplayCurrency()
{
if (OnToggleDisplayCurrency.HasDelegate)
await OnToggleDisplayCurrency.InvokeAsync();
}
}
4 changes: 4 additions & 0 deletions BTCPayApp.UI/Components/TransactionsListItem.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a {
display: block;
color: var(--btcpay-body-text);
}
29 changes: 29 additions & 0 deletions BTCPayApp.UI/Models/TransactionModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using BTCPayApp.Core.Data;
using BTCPayServer.Client.App;
using BTCPayServer.Lightning;

namespace BTCPayApp.UI.Models;

public enum TransactionPaymentMethod
{
Onchain,
Lightning
}

public enum TransactionType
{
Send,
Receive
}

public class TransactionModel
{
public string Id { get; set; }
public LightMoney Value { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string Status { get; set; }
public TransactionType Type { get; set; }
public TransactionPaymentMethod PaymentMethod { get; set; }
public AppLightningPayment? LightningPayment { get; set; }
public TxResp? OnchainTransaction { get; set; }
}
Loading

0 comments on commit 42d0b66

Please sign in to comment.