mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Fix: Recreating an aborted TX in MultiSig on Server setup crashes (#6682)
This commit is contained in:
@@ -8,6 +8,7 @@ namespace BTCPayServer.Data;
|
||||
|
||||
public class PendingTransaction: IHasBlob<PendingTransactionBlob>
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string TransactionId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
@@ -30,7 +31,9 @@ public class PendingTransaction: IHasBlob<PendingTransactionBlob>
|
||||
.HasForeignKey(i => i.StoreId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<PendingTransaction>().HasKey(transaction => new {transaction.CryptoCode, transaction.TransactionId});
|
||||
builder.Entity<PendingTransaction>().HasKey(t => t.Id);
|
||||
builder.Entity<PendingTransaction>().HasIndex(t => new { t.StoreId });
|
||||
builder.Entity<PendingTransaction>().HasIndex(t => new { t.TransactionId });
|
||||
|
||||
builder.Entity<PendingTransaction>()
|
||||
.Property(o => o.Blob2)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20250418074941_changependingtxsid")]
|
||||
public partial class changependingtxsid : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_PendingTransactions",
|
||||
table: "PendingTransactions");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "TransactionId",
|
||||
table: "PendingTransactions",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "CryptoCode",
|
||||
table: "PendingTransactions",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Id",
|
||||
table: "PendingTransactions",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "PendingTransactions" SET "Id" =
|
||||
lpad(to_hex(trunc(random() * 1e10)::bigint), 8, '0') || '-' ||
|
||||
lpad(to_hex(trunc(random() * 1e10)::bigint), 4, '0') || '-' ||
|
||||
lpad(to_hex(trunc(random() * 1e10)::bigint), 4, '0') || '-' ||
|
||||
lpad(to_hex(trunc(random() * 1e10)::bigint), 4, '0') || '-' ||
|
||||
lpad(to_hex(trunc(random() * 1e10)::bigint), 12, '0');
|
||||
""");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_PendingTransactions",
|
||||
table: "PendingTransactions",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PendingTransactions_TransactionId",
|
||||
table: "PendingTransactions",
|
||||
column: "TransactionId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
@@ -652,15 +652,15 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingTransaction", b =>
|
||||
{
|
||||
b.Property<string>("CryptoCode")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TransactionId")
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
.HasColumnType("JSONB");
|
||||
|
||||
b.Property<string>("CryptoCode")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset?>("Expiry")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -673,10 +673,15 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("CryptoCode", "TransactionId");
|
||||
b.Property<string>("TransactionId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.HasIndex("TransactionId");
|
||||
|
||||
b.ToTable("PendingTransactions");
|
||||
});
|
||||
|
||||
|
||||
@@ -558,8 +558,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (vm.SigningContext.PendingTransactionId is not null)
|
||||
{
|
||||
await _pendingTransactionService.Broadcasted(walletId.CryptoCode, walletId.StoreId,
|
||||
vm.SigningContext.PendingTransactionId);
|
||||
await _pendingTransactionService.Broadcasted(GetPendingTxId(walletId, vm.SigningContext.PendingTransactionId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vm.ReturnUrl))
|
||||
|
||||
@@ -145,39 +145,38 @@ namespace BTCPayServer.Controllers
|
||||
_displayFormatter = displayFormatter;
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/pending/{transactionId}/cancel")]
|
||||
[HttpGet("{walletId}/pending/{pendingTransactionId}/cancel")]
|
||||
public IActionResult CancelPendingTransaction(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
string transactionId)
|
||||
string pendingTransactionId)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel("Abort Pending Transaction",
|
||||
"Proceeding with this action will invalidate Pending Transaction and all accepted signatures.",
|
||||
"Confirm Abort"));
|
||||
}
|
||||
[HttpPost("{walletId}/pending/{transactionId}/cancel")]
|
||||
[HttpPost("{walletId}/pending/{pendingTransactionId}/cancel")]
|
||||
public async Task<IActionResult> CancelPendingTransactionConfirmed(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
string transactionId)
|
||||
string pendingTransactionId)
|
||||
{
|
||||
await _pendingTransactionService.CancelPendingTransaction(walletId.CryptoCode, walletId.StoreId, transactionId);
|
||||
await _pendingTransactionService.CancelPendingTransaction(GetPendingTxId(walletId, pendingTransactionId));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = $"Aborted Pending Transaction {transactionId}"
|
||||
Message = $"Aborted Pending Transaction {pendingTransactionId}"
|
||||
});
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{walletId}/pending/{transactionId}")]
|
||||
[HttpGet("{walletId}/pending/{pendingTransactionId}")]
|
||||
public async Task<IActionResult> ViewPendingTransaction(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
string transactionId)
|
||||
string pendingTransactionId)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var pendingTransaction =
|
||||
await _pendingTransactionService.GetPendingTransaction(walletId.CryptoCode, walletId.StoreId,
|
||||
transactionId);
|
||||
await _pendingTransactionService.GetPendingTransaction(GetPendingTxId(walletId, pendingTransactionId));
|
||||
if (pendingTransaction is null)
|
||||
return NotFound();
|
||||
var blob = pendingTransaction.GetBlob();
|
||||
@@ -197,7 +196,7 @@ namespace BTCPayServer.Controllers
|
||||
CryptoCode = network.CryptoCode,
|
||||
SigningContext = new SigningContextModel(currentPsbt)
|
||||
{
|
||||
PendingTransactionId = transactionId,
|
||||
PendingTransactionId = pendingTransactionId,
|
||||
PSBT = currentPsbt.ToBase64(),
|
||||
},
|
||||
};
|
||||
@@ -206,6 +205,10 @@ namespace BTCPayServer.Controllers
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
}
|
||||
|
||||
private PendingTransactionService.PendingTransactionFullId GetPendingTxId(WalletId walletId, string pendingTransactionId)
|
||||
=> new (walletId.CryptoCode, walletId.StoreId, pendingTransactionId);
|
||||
|
||||
|
||||
[Route("{walletId}/transactions/bump")]
|
||||
[Route("{walletId}/transactions/{transactionId}/bump")]
|
||||
public async Task<IActionResult> WalletBumpFee([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
@@ -1354,7 +1357,7 @@ namespace BTCPayServer.Controllers
|
||||
if (vm.SigningContext.PendingTransactionId is not null)
|
||||
{
|
||||
var psbt = PSBT.Parse(vm.SigningContext.PSBT, NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).NBitcoinNetwork);
|
||||
var pendingTransaction = await _pendingTransactionService.CollectSignature(psbt, CancellationToken.None);
|
||||
var pendingTransaction = await _pendingTransactionService.CollectSignature(GetPendingTxId(walletId, vm.SigningContext.PendingTransactionId), psbt, CancellationToken.None);
|
||||
|
||||
if (pendingTransaction != null)
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
|
||||
@@ -13,6 +13,7 @@ using BTCPayServer.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
@@ -58,11 +59,12 @@ public class PendingTransactionService(
|
||||
else if (evt is NewOnChainTransactionEvent newTransactionEvent)
|
||||
{
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var cryptoCode = newTransactionEvent.NewTransactionEvent.CryptoCode;
|
||||
var txInputs = newTransactionEvent.NewTransactionEvent.TransactionData.Transaction.Inputs
|
||||
.Select(i => i.PrevOut.ToString()).ToArray();
|
||||
var txHash = newTransactionEvent.NewTransactionEvent.TransactionData.TransactionHash.ToString();
|
||||
var pendingTransactions = await ctx.PendingTransactions
|
||||
.Where(p => p.TransactionId == txHash || p.OutpointsUsed.Any(o => txInputs.Contains(o)))
|
||||
.Where(p => p.CryptoCode == cryptoCode && (p.TransactionId == txHash || p.OutpointsUsed.Any(o => txInputs.Contains(o))))
|
||||
.ToArrayAsync(cancellationToken: cancellationToken);
|
||||
if (!pendingTransactions.Any())
|
||||
{
|
||||
@@ -119,6 +121,7 @@ public class PendingTransactionService(
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var pendingTransaction = new PendingTransaction
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
CryptoCode = cryptoCode,
|
||||
TransactionId = txId.ToString(),
|
||||
State = PendingTransactionState.Pending,
|
||||
@@ -147,13 +150,12 @@ public class PendingTransactionService(
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
public async Task<PendingTransaction?> CollectSignature(PSBT psbt, CancellationToken cancellationToken)
|
||||
public async Task<PendingTransaction?> CollectSignature(PendingTransactionFullId id, PSBT psbt, CancellationToken cancellationToken)
|
||||
{
|
||||
var cryptoCode = psbt.Network.NetworkSet.CryptoCode;
|
||||
var txId = psbt.GetGlobalTransaction().GetHash();
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var pendingTransaction =
|
||||
await ctx.PendingTransactions.FindAsync(new object[] { cryptoCode, txId.ToString() }, cancellationToken);
|
||||
var pendingTransaction = await ctx.PendingTransactions.FirstOrDefaultAsync(p =>
|
||||
p.CryptoCode == id.CryptoCode && p.StoreId == id.StoreId && p.Id == id.Id, cancellationToken);
|
||||
|
||||
if (pendingTransaction?.State is not PendingTransactionState.Pending)
|
||||
{
|
||||
return null;
|
||||
@@ -227,12 +229,12 @@ public class PendingTransactionService(
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<PendingTransaction?> GetPendingTransaction(string cryptoCode, string storeId, string txId)
|
||||
public record PendingTransactionFullId(string CryptoCode, string StoreId, string Id);
|
||||
public async Task<PendingTransaction?> GetPendingTransaction(PendingTransactionFullId id)
|
||||
{
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
return await ctx.PendingTransactions.FirstOrDefaultAsync(p =>
|
||||
p.CryptoCode == cryptoCode && p.StoreId == storeId && p.TransactionId == txId);
|
||||
p.CryptoCode == id.CryptoCode && p.StoreId == id.StoreId && p.Id == id.Id);
|
||||
}
|
||||
|
||||
public async Task<PendingTransaction[]> GetPendingTransactions(string cryptoCode, string storeId)
|
||||
@@ -244,11 +246,11 @@ public class PendingTransactionService(
|
||||
.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task CancelPendingTransaction(string cryptoCode, string storeId, string transactionId)
|
||||
public async Task CancelPendingTransaction(PendingTransactionFullId id)
|
||||
{
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var pt = await ctx.PendingTransactions.FirstOrDefaultAsync(p =>
|
||||
p.CryptoCode == cryptoCode && p.StoreId == storeId && p.TransactionId == transactionId &&
|
||||
p.CryptoCode == id.CryptoCode && p.StoreId == id.StoreId && p.Id == id.Id &&
|
||||
(p.State == PendingTransactionState.Pending || p.State == PendingTransactionState.Signed));
|
||||
if (pt is null) return;
|
||||
pt.State = PendingTransactionState.Cancelled;
|
||||
@@ -260,11 +262,11 @@ public class PendingTransactionService(
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Broadcasted(string cryptoCode, string storeId, string transactionId)
|
||||
public async Task Broadcasted(PendingTransactionFullId id)
|
||||
{
|
||||
await using var ctx = dbContextFactory.CreateContext();
|
||||
var pt = await ctx.PendingTransactions.FirstOrDefaultAsync(p =>
|
||||
p.CryptoCode == cryptoCode && p.StoreId == storeId && p.TransactionId == transactionId &&
|
||||
p.CryptoCode == id.CryptoCode && p.StoreId == id.StoreId && p.Id == id.Id &&
|
||||
(p.State == PendingTransactionState.Pending || p.State == PendingTransactionState.Signed));
|
||||
if (pt is null) return;
|
||||
pt.State = PendingTransactionState.Broadcast;
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
else if (Model.SigningContext.PendingTransactionId is not null)
|
||||
{
|
||||
<a asp-action="CancelPendingTransaction" asp-route-walletId="@walletId"
|
||||
asp-route-transactionId="@Model.SigningContext.PendingTransactionId" class="btn btn-danger">Cancel</a>
|
||||
asp-route-pendingTransactionId="@Model.SigningContext.PendingTransactionId" class="btn btn-danger">Cancel</a>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -191,10 +191,10 @@
|
||||
<td><span id="Sigs_@(index)__Scheme">@ptblob?.SignaturesNeeded/@ptblob?.SignaturesTotal</span></td>
|
||||
<td>
|
||||
<a asp-action="ViewPendingTransaction" asp-route-walletId="@walletId"
|
||||
asp-route-transactionId="@pendingTransaction.TransactionId">@(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View")</a>
|
||||
asp-route-pendingTransactionId="@pendingTransaction.Id">@(pendingTransaction.State == PendingTransactionState.Signed ? "Broadcast" : "View")</a>
|
||||
-
|
||||
<a asp-action="CancelPendingTransaction" asp-route-walletId="@walletId"
|
||||
asp-route-transactionId="@pendingTransaction.TransactionId">Abort</a>
|
||||
asp-route-pendingTransactionId="@pendingTransaction.Id">Abort</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user