mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-04 09:58:13 +01:00
Refactor labels (#4179)
* Create new tables * wip * wip * Refactor LegacyLabel * Remove LabelFactory * Add migration * wip * wip * Add pull-payment attachment to tx * Address kukks points
This commit is contained in:
parent
895462ac7f
commit
a2fa688cde
38 changed files with 1303 additions and 729 deletions
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
[Obsolete]
|
||||
public class LabelData
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
|
|
@ -13,7 +13,9 @@ namespace BTCPayServer.Client.Models
|
|||
public uint256 TransactionHash { get; set; }
|
||||
|
||||
public string Comment { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
|
|
@ -15,7 +15,9 @@ namespace BTCPayServer.Client.Models
|
|||
[JsonConverter(typeof(OutpointJsonConverter))]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
public string Link { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; }
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
|
|
|
@ -59,7 +59,11 @@ namespace BTCPayServer.Data
|
|||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||
public DbSet<UserStore> UserStore { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||
public DbSet<WalletObjectLinkData> WalletObjectLinks { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
||||
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
|
||||
public DbSet<WebhookData> Webhooks { get; set; }
|
||||
|
@ -109,7 +113,11 @@ namespace BTCPayServer.Data
|
|||
Fido2Credential.OnModelCreating(builder);
|
||||
BTCPayServer.Data.UserStore.OnModelCreating(builder);
|
||||
//WalletData.OnModelCreating(builder);
|
||||
WalletObjectData.OnModelCreating(builder, Database);
|
||||
WalletObjectLinkData.OnModelCreating(builder, Database);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
WalletTransactionData.OnModelCreating(builder);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
WebhookDeliveryData.OnModelCreating(builder);
|
||||
LightningAddressData.OnModelCreating(builder);
|
||||
PayoutProcessorData.OnModelCreating(builder);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletData
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
|
|
44
BTCPayServer.Data/Data/WalletObjectData.cs
Normal file
44
BTCPayServer.Data/Data/WalletObjectData.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectData
|
||||
{
|
||||
public class Types
|
||||
{
|
||||
public const string Label = "label";
|
||||
public const string Tx = "tx";
|
||||
}
|
||||
public string WalletId { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public List<WalletObjectLinkData> ChildLinks { get; set; }
|
||||
public List<WalletObjectLinkData> ParentLinks { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.Type,
|
||||
o.Id,
|
||||
});
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
BTCPayServer.Data/Data/WalletObjectLinkData.cs
Normal file
61
BTCPayServer.Data/Data/WalletObjectLinkData.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectLinkData
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string ParentType { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
public string ChildType { get; set; }
|
||||
public string ChildId { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public WalletObjectData Parent { get; set; }
|
||||
public WalletObjectData Child { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ParentType,
|
||||
o.ParentId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
});
|
||||
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
});
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Parent)
|
||||
.WithMany(o => o.ChildLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Child)
|
||||
.WithMany(o => o.ParentLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletTransactionData
|
||||
{
|
||||
public string WalletDataId { get; set; }
|
||||
|
|
77
BTCPayServer.Data/Migrations/20220929132704_label.cs
Normal file
77
BTCPayServer.Data/Migrations/20220929132704_label.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
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.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20220929132704_label")]
|
||||
public partial class label : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjects",
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjects", x => new { x.WalletId, x.Type, x.Id });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjectLinks",
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
|
||||
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
|
||||
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
|
||||
table: "WalletObjectLinks",
|
||||
columns: new[] { "WalletId", "ChildType", "ChildId" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjectLinks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjects");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
|
@ -189,6 +189,7 @@ namespace BTCPayServer.Migrations
|
|||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -845,6 +846,52 @@ namespace BTCPayServer.Migrations
|
|||
b.ToTable("Wallets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "Type", "Id");
|
||||
|
||||
b.ToTable("WalletObjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
|
||||
|
||||
b.HasIndex("WalletId", "ChildType", "ChildId");
|
||||
|
||||
b.ToTable("WalletObjectLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId")
|
||||
|
@ -1333,6 +1380,25 @@ namespace BTCPayServer.Migrations
|
|||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
|
||||
.WithMany("ParentLinks")
|
||||
.HasForeignKey("WalletId", "ChildType", "ChildId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
|
||||
.WithMany("ChildLinks")
|
||||
.HasForeignKey("WalletId", "ParentType", "ParentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Child");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
|
@ -1475,6 +1541,13 @@ namespace BTCPayServer.Migrations
|
|||
b.Navigation("WalletTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Navigation("ChildLinks");
|
||||
|
||||
b.Navigation("ParentLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
|
||||
{
|
||||
b.Navigation("Deliveries");
|
||||
|
|
|
@ -483,93 +483,6 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void CanParseLegacyLabels()
|
||||
{
|
||||
static void AssertContainsRawLabel(WalletTransactionInfo info)
|
||||
{
|
||||
foreach (var item in new[] { "blah", "lol", "hello" })
|
||||
{
|
||||
Assert.True(info.Labels.ContainsKey(item));
|
||||
var rawLabel = Assert.IsType<RawLabel>(info.Labels[item]);
|
||||
Assert.Equal("raw", rawLabel.Type);
|
||||
Assert.Equal(item, rawLabel.Text);
|
||||
}
|
||||
}
|
||||
var data = new WalletTransactionData();
|
||||
data.Labels = "blah,lol,hello,lol";
|
||||
var info = data.GetBlobInfo();
|
||||
Assert.Equal(3, info.Labels.Count);
|
||||
AssertContainsRawLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
Assert.Contains("raw", data.Labels);
|
||||
Assert.Contains("{", data.Labels);
|
||||
Assert.Contains("[", data.Labels);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsRawLabel(info);
|
||||
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "pos",
|
||||
Blob = Encoders.Hex.DecodeData("1f8b08000000000000037abf7b7fb592737e6e6e6a5e89929592522d000000ffff030036bc6ad911000000")
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
var label = Assert.Single(info.Labels);
|
||||
Assert.Equal("raw", label.Value.Type);
|
||||
Assert.Equal("pos", label.Value.Text);
|
||||
Assert.Equal("pos", label.Key);
|
||||
|
||||
|
||||
static void AssertContainsLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Equal(2, info.Labels.Count);
|
||||
var invoiceLabel = Assert.IsType<ReferenceLabel>(info.Labels["invoice"]);
|
||||
Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference);
|
||||
Assert.Equal("invoice", invoiceLabel.Text);
|
||||
Assert.Equal("invoice", invoiceLabel.Type);
|
||||
|
||||
var appLabel = Assert.IsType<ReferenceLabel>(info.Labels["app"]);
|
||||
Assert.Equal("87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe", appLabel.Reference);
|
||||
Assert.Equal("app", appLabel.Text);
|
||||
Assert.Equal("app", appLabel.Type);
|
||||
}
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "[\"{\\n \\\"value\\\": \\\"invoice\\\",\\n \\\"id\\\": \\\"BFm1MCJPBCDeRoWXvPcwnM\\\"\\n}\",\"{\\n \\\"value\\\": \\\"app\\\",\\n \\\"id\\\": \\\"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\\\"\\n}\"]",
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
|
||||
static void AssertPayoutLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Single(info.Labels);
|
||||
var l = Assert.IsType<PayoutLabel>(info.Labels["payout"]);
|
||||
Assert.Single(Assert.Single(l.PullPaymentPayouts, k => k.Key == "pullPaymentId").Value, "payoutId");
|
||||
Assert.Equal("walletId", l.WalletId);
|
||||
}
|
||||
|
||||
var payoutId = "payoutId";
|
||||
var pullPaymentId = "pullPaymentId";
|
||||
var walletId = "walletId";
|
||||
// How it was serialized before
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = new JArray(JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId })).ToString()
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DeterministicUTXOSorter()
|
||||
{
|
||||
|
|
|
@ -2270,6 +2270,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
|
||||
|
||||
// transaction patch tests
|
||||
|
@ -2290,7 +2291,7 @@ namespace BTCPayServer.Tests
|
|||
}.ToJson(),
|
||||
patchedTransaction.Labels.ToJson()
|
||||
);
|
||||
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
|
||||
|
|
|
@ -40,6 +40,7 @@ using BTCPayServer.Security.Bitpay;
|
|||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Storage.Models;
|
||||
|
@ -51,6 +52,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
|
@ -785,9 +787,9 @@ namespace BTCPayServer.Tests
|
|||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Tags.GroupBy(l => l.Color).Count());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
|
@ -797,12 +799,9 @@ namespace BTCPayServer.Tests
|
|||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Single(tx.Labels.GroupBy(l => l.Color));
|
||||
|
||||
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);
|
||||
Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Single(tx.Tags.GroupBy(l => l.Color));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
@ -2521,6 +2520,79 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(lnMethod.IsInternalNode);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Obsolete]
|
||||
public async Task CanDoLabelMigrations()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var dbf = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
int walletCount = 1000;
|
||||
var wallet = "walletttttttttttttttttttttttttttt";
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
for (int i = 0; i < walletCount; i++)
|
||||
{
|
||||
var walletData = new WalletData() { Id = $"S-{wallet}{i}-BTC" };
|
||||
walletData.Blob = ZipUtils.Zip("{\"LabelColors\": { \"label1\" : \"black\", \"payout\":\"green\" }}");
|
||||
db.Wallets.Add(walletData);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
uint256 firstTxId = null;
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
int transactionCount = 10_000;
|
||||
for (int i = 0; i < transactionCount; i++)
|
||||
{
|
||||
var txId = RandomUtils.GetUInt256();
|
||||
var wt = new WalletTransactionData()
|
||||
{
|
||||
WalletDataId = $"S-{wallet}{i % walletCount}-BTC",
|
||||
TransactionId = txId.ToString(),
|
||||
};
|
||||
firstTxId ??= txId;
|
||||
if (i != 10)
|
||||
wt.Blob = ZipUtils.Zip("{\"Comment\":\"test\"}");
|
||||
if (i % 1240 != 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"}]";
|
||||
}
|
||||
else if (i == 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}, " +
|
||||
"{\"type\":\"payout\", \"text\":\"payout\", \"pullPaymentPayouts\":{\"pp1\":[\"p1\",\"p2\"],\"pp2\":[\"p3\"]}}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}]";
|
||||
}
|
||||
db.WalletTransactions.Add(wt);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await RestartMigration(tester);
|
||||
var migrator = tester.PayTester.GetService<IEnumerable<IHostedService>>().OfType<DbMigrationsHostedService>().First();
|
||||
await migrator.MigratedTransactionLabels(0);
|
||||
|
||||
var walletRepo = tester.PayTester.GetService<WalletRepository>();
|
||||
var wi1 = await walletRepo.GetWalletLabels(new WalletId($"{wallet}0", "BTC"));
|
||||
Assert.Equal(3, wi1.Length);
|
||||
Assert.Contains(wi1, o => o.Label == "label1" && o.Color == "black");
|
||||
Assert.Contains(wi1, o => o.Label == "labelo0" && o.Color == "#000");
|
||||
Assert.Contains(wi1, o => o.Label == "payout" && o.Color == "green");
|
||||
|
||||
var txInfo = await walletRepo.GetWalletTransactionsInfo(new WalletId($"{wallet}0", "BTC"), new[] { firstTxId.ToString() });
|
||||
Assert.Equal("test", txInfo.Values.First().Comment);
|
||||
// Should have the 2 raw labels, and one legacy label for payouts
|
||||
Assert.Equal(3, txInfo.Values.First().LegacyLabels.Count);
|
||||
var payoutLabel = txInfo.Values.First().LegacyLabels.Select(l => l.Value).OfType<PayoutLabel>().First();
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts.Count);
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts["pp1"].Count);
|
||||
Assert.Single(payoutLabel.PullPaymentPayouts["pp2"]);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
|
57
BTCPayServer/ColorPalette.cs
Normal file
57
BTCPayServer/ColorPalette.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ColorPalette
|
||||
{
|
||||
public string TextColor(string bgColor)
|
||||
{
|
||||
int nThreshold = 105;
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
"#fbca04",
|
||||
"#0e8a16",
|
||||
"#ff7619",
|
||||
"#84b6eb",
|
||||
"#5319e7",
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
});
|
||||
private ColorPalette(string[] labels)
|
||||
{
|
||||
Labels = labels;
|
||||
}
|
||||
|
||||
public readonly string[] Labels;
|
||||
|
||||
public string DeterministicColor(string label)
|
||||
{
|
||||
switch (label)
|
||||
{
|
||||
case "payjoin":
|
||||
return "#51b13e";
|
||||
case "invoice":
|
||||
return "#cedc21";
|
||||
case "payment-request":
|
||||
return "#489D77";
|
||||
case "app":
|
||||
return "#5093B6";
|
||||
case "pj-exposed":
|
||||
return "#51b13e";
|
||||
case "payout":
|
||||
return "#3F88AF";
|
||||
default:
|
||||
var num = NBitcoin.Utils.ToUInt32(Hashes.SHA256(Encoding.UTF8.GetBytes(label)), 0, true);
|
||||
return Labels[num % Labels.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
private readonly LabelFactory _labelFactory;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
|
||||
public GreenfieldStoreOnChainWalletsController(
|
||||
|
@ -69,7 +68,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
EventAggregator eventAggregator,
|
||||
WalletReceiveService walletReceiveService,
|
||||
IFeeProviderFactory feeProviderFactory,
|
||||
LabelFactory labelFactory,
|
||||
UTXOLocker utxoLocker
|
||||
)
|
||||
{
|
||||
|
@ -86,7 +84,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
_eventAggregator = eventAggregator;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
_labelFactory = labelFactory;
|
||||
_utxoLocker = utxoLocker;
|
||||
}
|
||||
|
||||
|
@ -202,7 +199,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
if (!string.IsNullOrWhiteSpace(labelFilter))
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(t.TransactionId.ToString(), out var transactionInfo);
|
||||
if (transactionInfo?.Labels.ContainsKey(labelFilter) is true)
|
||||
if (transactionInfo?.LabelColors.ContainsKey(labelFilter) is true)
|
||||
filteredList.Add(t);
|
||||
}
|
||||
if (statusFilter?.Any() is true)
|
||||
|
@ -270,36 +267,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
}
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
if (!(await walletTransactionsInfoAsync).TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
var txObjectId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
|
||||
|
||||
if (request.Comment != null)
|
||||
{
|
||||
walletTransactionInfo.Comment = request.Comment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
|
||||
await _walletRepository.SetWalletObjectComment(txObjectId, request.Comment);
|
||||
}
|
||||
|
||||
if (request.Labels != null)
|
||||
{
|
||||
var walletBlobInfo = await _walletRepository.GetWalletInfo(walletId);
|
||||
|
||||
foreach (string label in request.Labels)
|
||||
{
|
||||
var rawLabel = await _labelFactory.BuildLabel(
|
||||
walletBlobInfo,
|
||||
Request,
|
||||
walletTransactionInfo,
|
||||
walletId,
|
||||
transactionId,
|
||||
label
|
||||
);
|
||||
walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel);
|
||||
}
|
||||
await _walletRepository.AddWalletObjectLabels(txObjectId, request.Labels.ToArray());
|
||||
}
|
||||
|
||||
await _walletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
var walletTransactionsInfo =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId }))
|
||||
.Values
|
||||
|
@ -319,19 +298,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = info?.Labels ?? new Dictionary<string, LabelData>();
|
||||
|
||||
return new OnChainWalletUTXOData()
|
||||
{
|
||||
Outpoint = coin.OutPoint,
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = info?.Labels,
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Labels = info?.LegacyLabels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
coin.OutPoint.Hash.ToString()),
|
||||
Timestamp = coin.Timestamp,
|
||||
|
@ -592,8 +572,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
payjoinPSBT.Finalize();
|
||||
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||
var hash = payjoinTransaction.GetHash();
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
|
||||
UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode), hash, Attachment.Payjoin());
|
||||
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
|
@ -676,7 +655,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||
{
|
||||
TransactionHash = tx.TransactionId,
|
||||
Comment = walletTransactionsInfoAsync?.Comment ?? string.Empty,
|
||||
Labels = walletTransactionsInfoAsync?.Labels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Labels = walletTransactionsInfoAsync?.LegacyLabels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
Amount = tx.BalanceChange.GetValue(wallet.Network),
|
||||
BlockHash = tx.BlockHash,
|
||||
BlockHeight = tx.Height,
|
||||
|
|
|
@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers
|
|||
vm.SigningContext.OriginalPSBT = psbt.ToBase64();
|
||||
proposedPayjoin.Finalize();
|
||||
var hash = proposedPayjoin.ExtractTransaction().GetHash();
|
||||
_EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||
await WalletRepository.AddWalletTransactionAttachment(walletId, hash, Attachment.Payjoin());
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
|
|
|
@ -39,6 +39,8 @@ using NBXplorer.DerivationStrategy;
|
|||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -60,11 +62,10 @@ namespace BTCPayServer.Controllers
|
|||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly DelayedTransactionBroadcaster _broadcaster;
|
||||
private readonly PayjoinClient _payjoinClient;
|
||||
private readonly LabelFactory _labelFactory;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
@ -84,16 +85,16 @@ namespace BTCPayServer.Controllers
|
|||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletReceiveService walletReceiveService,
|
||||
EventAggregator eventAggregator,
|
||||
SettingsRepository settingsRepository,
|
||||
DelayedTransactionBroadcaster broadcaster,
|
||||
PayjoinClient payjoinClient,
|
||||
LabelFactory labelFactory,
|
||||
IServiceProvider serviceProvider,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
UTXOLocker utxoLocker)
|
||||
UTXOLocker utxoLocker,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
_linkGenerator = linkGenerator;
|
||||
Repository = repo;
|
||||
WalletRepository = walletRepository;
|
||||
RateFetcher = rateProvider;
|
||||
|
@ -105,11 +106,9 @@ namespace BTCPayServer.Controllers
|
|||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_settingsRepository = settingsRepository;
|
||||
_broadcaster = broadcaster;
|
||||
_payjoinClient = payjoinClient;
|
||||
_labelFactory = labelFactory;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_utxoLocker = utxoLocker;
|
||||
ServiceProvider = serviceProvider;
|
||||
|
@ -146,58 +145,19 @@ namespace BTCPayServer.Controllers
|
|||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var walletBlobInfoAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobInfo = await walletBlobInfoAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
if (addlabel != null)
|
||||
{
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
|
||||
var rawLabel = await _labelFactory.BuildLabel(
|
||||
walletBlobInfo,
|
||||
Request!,
|
||||
walletTransactionInfo,
|
||||
walletId,
|
||||
transactionId,
|
||||
addlabel
|
||||
);
|
||||
if (walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel))
|
||||
{
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
await WalletRepository.AddWalletObjectLabels(txObjId, addlabel);
|
||||
}
|
||||
else if (removelabel != null)
|
||||
{
|
||||
removelabel = removelabel.Trim();
|
||||
if (walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
if (walletTransactionInfo.Labels.Remove(removelabel))
|
||||
{
|
||||
var canDeleteColor =
|
||||
!walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
|
||||
if (canDeleteColor)
|
||||
{
|
||||
walletBlobInfo.LabelColors.Remove(removelabel);
|
||||
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
|
||||
}
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
}
|
||||
await WalletRepository.RemoveWalletObjectLabels(txObjId, removelabel);
|
||||
}
|
||||
else if (addcomment != null)
|
||||
{
|
||||
addcomment = addcomment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
walletTransactionInfo.Comment = addcomment;
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
await WalletRepository.SetWalletObjectComment(txObjId, addcomment);
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
@ -267,15 +227,17 @@ namespace BTCPayServer.Controllers
|
|||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
|
||||
// We can't filter at the database level if we need to apply label filter
|
||||
var preFiltering = string.IsNullOrEmpty(labelFilter);
|
||||
var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null);
|
||||
var walletBlob = await walletBlobAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))
|
||||
);
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
|
||||
|
@ -305,14 +267,13 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
|
||||
{
|
||||
var labels = _labelFactory.ColorizeTransactionLabels(walletBlob, transactionInfo, Request);
|
||||
vm.Labels.AddRange(labels);
|
||||
model.Labels.AddRange(labels);
|
||||
var labels = CreateTransactionTagModels(transactionInfo);
|
||||
vm.Tags.AddRange(labels);
|
||||
vm.Comment = transactionInfo.Comment;
|
||||
}
|
||||
|
||||
if (labelFilter == null ||
|
||||
vm.Labels.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
vm.Tags.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
model.Transactions.Add(vm);
|
||||
}
|
||||
|
||||
|
@ -613,17 +574,15 @@ namespace BTCPayServer.Controllers
|
|||
var schemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (schemeSettings is null)
|
||||
return NotFound();
|
||||
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
|
||||
var utxos = await _walletProvider.GetWallet(network)
|
||||
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
||||
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
|
||||
vm.InputsAvailable = utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = info?.Labels == null
|
||||
? new List<ColoredLabel>()
|
||||
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request).ToList();
|
||||
var labels = CreateTransactionTagModels(info).ToList();
|
||||
return new WalletSendModel.InputSelectionOption()
|
||||
{
|
||||
Outpoint = coin.OutPoint.ToString(),
|
||||
|
@ -1359,6 +1318,117 @@ namespace BTCPayServer.Controllers
|
|||
private string GetUserId() => _userManager.GetUserId(User);
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
|
||||
public IEnumerable<TransactionTagModel> CreateTransactionTagModels(WalletTransactionInfo? transactionInfo)
|
||||
{
|
||||
if (transactionInfo is null)
|
||||
return Array.Empty<TransactionTagModel>();
|
||||
|
||||
string PayoutTooltip(IGrouping<string, string>? payoutsByPullPaymentId = null)
|
||||
{
|
||||
if (payoutsByPullPaymentId is null)
|
||||
{
|
||||
return "Paid a payout";
|
||||
}
|
||||
else if (payoutsByPullPaymentId.Count() == 1)
|
||||
{
|
||||
var pp = payoutsByPullPaymentId.Key;
|
||||
var payout = payoutsByPullPaymentId.First();
|
||||
if (!string.IsNullOrEmpty(pp))
|
||||
return $"Paid a payout ({payout}) of a pull payment ({pp})";
|
||||
else
|
||||
return $"Paid a payout {payout}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var pp = payoutsByPullPaymentId.Key;
|
||||
if (!string.IsNullOrEmpty(pp))
|
||||
return $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})";
|
||||
else
|
||||
return $"Paid {payoutsByPullPaymentId.Count()} payouts";
|
||||
}
|
||||
}
|
||||
|
||||
var models = new Dictionary<string, TransactionTagModel>();
|
||||
foreach (var tag in transactionInfo.Attachments)
|
||||
{
|
||||
if (models.ContainsKey(tag.Type))
|
||||
continue;
|
||||
if (!transactionInfo.LabelColors.TryGetValue(tag.Type, out var color))
|
||||
continue;
|
||||
var model = new TransactionTagModel
|
||||
{
|
||||
Text = tag.Type,
|
||||
Color = color,
|
||||
TextColor = ColorPalette.Default.TextColor(color)
|
||||
};
|
||||
models.Add(tag.Type, model);
|
||||
if (tag.Type == "payout")
|
||||
{
|
||||
var payoutsByPullPaymentId =
|
||||
transactionInfo.Attachments.Where(t => t.Type == "payout")
|
||||
.GroupBy(t => t.Data?["pullPaymentId"]?.Value<string>() ?? "",
|
||||
k => k.Id).ToList();
|
||||
|
||||
model.Tooltip = payoutsByPullPaymentId.Count switch
|
||||
{
|
||||
0 => PayoutTooltip(),
|
||||
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
|
||||
_ =>
|
||||
$"<ul>{string.Join(string.Empty, payoutsByPullPaymentId.Select(pair => $"<li>{PayoutTooltip(pair)}</li>"))}</ul>"
|
||||
};
|
||||
|
||||
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null, PayoutState.Completed, Request.Scheme, Request.Host,
|
||||
Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "payjoin")
|
||||
{
|
||||
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
else if (tag.Type == "invoice")
|
||||
{
|
||||
model.Tooltip = $"Received through an invoice {tag.Id}";
|
||||
model.Link = string.IsNullOrEmpty(tag.Id)
|
||||
? null
|
||||
: _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "payment-request")
|
||||
{
|
||||
model.Tooltip = $"Received through a payment request {tag.Id}";
|
||||
model.Link = _linkGenerator.PaymentRequestLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "app")
|
||||
{
|
||||
model.Tooltip = $"Received through an app {tag.Id}";
|
||||
model.Link = _linkGenerator.AppLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "pj-exposed")
|
||||
{
|
||||
|
||||
if (tag.Id.Length != 0)
|
||||
{
|
||||
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})";
|
||||
model.Link = _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else
|
||||
{
|
||||
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal";
|
||||
}
|
||||
}
|
||||
else if (tag.Type == "payjoin")
|
||||
{
|
||||
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
}
|
||||
foreach (var label in transactionInfo.LabelColors)
|
||||
models.TryAdd(label.Key, new TransactionTagModel
|
||||
{
|
||||
Text = label.Key,
|
||||
Color = label.Value,
|
||||
TextColor = ColorPalette.Default.TextColor(label.Value)
|
||||
});
|
||||
return models.Values.OrderBy(v => v.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletReceiveViewModel
|
||||
|
|
|
@ -34,22 +34,24 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly Logs Logs;
|
||||
|
||||
public WalletRepository WalletRepository { get; }
|
||||
|
||||
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
WalletRepository walletRepository,
|
||||
ExplorerClientProvider explorerClientProvider,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
EventAggregator eventAggregator,
|
||||
NotificationSender notificationSender,
|
||||
Logs logs)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
WalletRepository = walletRepository;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_notificationSender = notificationSender;
|
||||
this.Logs = logs;
|
||||
}
|
||||
|
@ -426,13 +428,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
|||
if (isInternal)
|
||||
{
|
||||
payout.State = PayoutState.InProgress;
|
||||
var walletId = new WalletId(payout.StoreDataId, newTransaction.CryptoCode);
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(walletId,
|
||||
await WalletRepository.AddWalletTransactionAttachment(
|
||||
new WalletId(payout.StoreDataId, newTransaction.CryptoCode),
|
||||
newTransaction.NewTransactionEvent.TransactionData.TransactionHash,
|
||||
UpdateTransactionLabel.PayoutTemplate(new ()
|
||||
{
|
||||
{payout.PullPaymentDataId?? "", new List<string>{payout.Id}}
|
||||
}, walletId.ToString())));
|
||||
Attachment.Payout(payout.PullPaymentDataId, payout.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public static class WalletDataExtensions
|
||||
{
|
||||
public static WalletBlobInfo GetBlobInfo(this WalletData walletData)
|
||||
{
|
||||
if (walletData.Blob == null || walletData.Blob.Length == 0)
|
||||
{
|
||||
return new WalletBlobInfo();
|
||||
}
|
||||
var blobInfo = JsonConvert.DeserializeObject<WalletBlobInfo>(ZipUtils.Unzip(walletData.Blob));
|
||||
return blobInfo;
|
||||
}
|
||||
public static void SetBlobInfo(this WalletData walletData, WalletBlobInfo blobInfo)
|
||||
{
|
||||
if (blobInfo == null)
|
||||
{
|
||||
walletData.Blob = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
walletData.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletTransactionInfo
|
||||
{
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||
}
|
||||
public static class WalletTransactionDataExtensions
|
||||
{
|
||||
public static int MaxCommentSize = 200;
|
||||
|
||||
public static WalletTransactionInfo GetBlobInfo(this WalletTransactionData walletTransactionData)
|
||||
{
|
||||
WalletTransactionInfo blobInfo;
|
||||
if (walletTransactionData.Blob == null || walletTransactionData.Blob.Length == 0)
|
||||
blobInfo = new WalletTransactionInfo();
|
||||
else
|
||||
blobInfo = JsonConvert.DeserializeObject<WalletTransactionInfo>(ZipUtils.Unzip(walletTransactionData.Blob));
|
||||
if (!string.IsNullOrEmpty(walletTransactionData.Labels))
|
||||
{
|
||||
if (walletTransactionData.Labels.StartsWith('['))
|
||||
{
|
||||
foreach (var jtoken in JArray.Parse(walletTransactionData.Labels))
|
||||
{
|
||||
var l = jtoken.Type == JTokenType.String ? Label.Parse(jtoken.Value<string>())
|
||||
: Label.Parse(jtoken.ToString());
|
||||
blobInfo.Labels.TryAdd(l.Text, l);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy path
|
||||
foreach (var token in walletTransactionData.Labels.Split(',',
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var l = Label.Parse(token);
|
||||
blobInfo.Labels.TryAdd(l.Text, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
return blobInfo;
|
||||
}
|
||||
static JsonSerializerSettings LabelSerializerSettings = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
public static void SetBlobInfo(this WalletTransactionData walletTransactionData, WalletTransactionInfo blobInfo)
|
||||
{
|
||||
if (blobInfo == null)
|
||||
{
|
||||
walletTransactionData.Labels = string.Empty;
|
||||
walletTransactionData.Blob = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
walletTransactionData.Labels = new JArray(
|
||||
blobInfo.Labels.Select(l => JsonConvert.SerializeObject(l.Value, LabelSerializerSettings))
|
||||
.Select(l => JObject.Parse(l))
|
||||
.OfType<JToken>()
|
||||
.ToArray()).ToString();
|
||||
walletTransactionData.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo));
|
||||
}
|
||||
}
|
||||
}
|
87
BTCPayServer/Data/WalletTransactionInfo.cs
Normal file
87
BTCPayServer/Data/WalletTransactionInfo.cs
Normal file
|
@ -0,0 +1,87 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletTransactionInfo
|
||||
{
|
||||
|
||||
public WalletTransactionInfo(WalletId walletId)
|
||||
{
|
||||
WalletId = walletId;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public WalletId WalletId { get; }
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public List<Attachment> Attachments { get; set; } = new List<Attachment>();
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, string> LabelColors { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
[Obsolete]
|
||||
Dictionary<string, LabelData>? _LegacyLabels;
|
||||
[JsonIgnore]
|
||||
[Obsolete]
|
||||
public Dictionary<string, LabelData> LegacyLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_LegacyLabels is null)
|
||||
{
|
||||
var legacyLabels = new Dictionary<string, LabelData>();
|
||||
foreach (var tag in Attachments)
|
||||
{
|
||||
switch (tag.Type)
|
||||
{
|
||||
case "payout":
|
||||
PayoutLabel legacyPayoutLabel;
|
||||
if (legacyLabels.TryGetValue(tag.Type, out var existing) &&
|
||||
existing is PayoutLabel)
|
||||
{
|
||||
legacyPayoutLabel = (PayoutLabel)existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
legacyPayoutLabel = new PayoutLabel();
|
||||
legacyLabels.Add(tag.Type, legacyPayoutLabel);
|
||||
}
|
||||
var ppid = tag.Data?["pullPaymentId"]?.Value<string>() ?? "";
|
||||
if (!legacyPayoutLabel.PullPaymentPayouts.TryGetValue(ppid, out var payouts))
|
||||
{
|
||||
payouts = new List<string>();
|
||||
legacyPayoutLabel.PullPaymentPayouts.Add(ppid, payouts);
|
||||
}
|
||||
payouts.Add(tag.Id);
|
||||
break;
|
||||
case "payjoin":
|
||||
case "payment-request":
|
||||
case "app":
|
||||
case "pj-exposed":
|
||||
case "invoice":
|
||||
legacyLabels.TryAdd(tag.Type, new ReferenceLabel(tag.Type, tag.Id));
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foreach (var label in LabelColors)
|
||||
{
|
||||
legacyLabels.TryAdd(label.Key, new RawLabel(label.Key));
|
||||
}
|
||||
_LegacyLabels = legacyLabels;
|
||||
}
|
||||
return _LegacyLabels;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,15 +2,20 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
@ -45,10 +50,235 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
await MigratedInvoiceTextSearchToDb(settings.MigratedInvoiceTextSearchPages ?? 0);
|
||||
}
|
||||
|
||||
// Refresh settings since these operations may run for very long time
|
||||
if (settings.MigratedTransactionLabels != int.MaxValue)
|
||||
{
|
||||
await MigratedTransactionLabels(settings.MigratedTransactionLabels ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
class LegacyWalletTransactionInfo
|
||||
{
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||
}
|
||||
|
||||
static LegacyWalletTransactionInfo GetBlobInfo(WalletTransactionData walletTransactionData)
|
||||
{
|
||||
LegacyWalletTransactionInfo blobInfo;
|
||||
if (walletTransactionData.Blob == null || walletTransactionData.Blob.Length == 0)
|
||||
blobInfo = new LegacyWalletTransactionInfo();
|
||||
else
|
||||
blobInfo = JsonConvert.DeserializeObject<LegacyWalletTransactionInfo>(ZipUtils.Unzip(walletTransactionData.Blob));
|
||||
if (!string.IsNullOrEmpty(walletTransactionData.Labels))
|
||||
{
|
||||
if (walletTransactionData.Labels.StartsWith('['))
|
||||
{
|
||||
foreach (var jtoken in JArray.Parse(walletTransactionData.Labels))
|
||||
{
|
||||
var l = jtoken.Type == JTokenType.String ? Label.Parse(jtoken.Value<string>())
|
||||
: Label.Parse(jtoken.ToString());
|
||||
blobInfo.Labels.TryAdd(l.Text, l);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy path
|
||||
foreach (var token in walletTransactionData.Labels.Split(',',
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var l = Label.Parse(token);
|
||||
blobInfo.Labels.TryAdd(l.Text, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
return blobInfo;
|
||||
}
|
||||
|
||||
internal async Task MigratedTransactionLabels(int startFromOffset)
|
||||
{
|
||||
// Only of 1000, that's what EF does anyway under the hood by default
|
||||
int batchCount = 1000;
|
||||
int total = 0;
|
||||
HashSet<(string WalletId, string LabelId)> existingLabels;
|
||||
using (var db = _dbContextFactory.CreateContext())
|
||||
{
|
||||
total = await db.WalletTransactions.CountAsync();
|
||||
existingLabels = (await (
|
||||
db.WalletObjects.AsNoTracking()
|
||||
.Where(wo => wo.Type == WalletObjectData.Types.Label)
|
||||
.Select(wl => new { wl.WalletId, wl.Id })
|
||||
.ToListAsync()))
|
||||
.Select(o => (o.WalletId, o.Id)).ToHashSet();
|
||||
}
|
||||
|
||||
|
||||
|
||||
next:
|
||||
// var insertedObjectInDBContext
|
||||
// Need to keep track of this hack, or then EF has a bug where he crash on the .Add and get internally
|
||||
// corrupted.
|
||||
var ifuckinghateentityframework = new HashSet<(string WalletId, string Type, string Id)>();
|
||||
using (var db = _dbContextFactory.CreateContext())
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Wallet transaction label importing transactions {startFromOffset}/{total}");
|
||||
var txs = await db.WalletTransactions
|
||||
.OrderByDescending(wt => wt.WalletDataId).ThenBy(wt => wt.TransactionId)
|
||||
.Skip(startFromOffset)
|
||||
.Take(batchCount)
|
||||
.ToArrayAsync();
|
||||
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
// Same as above
|
||||
var ifuckinghateentityframework2 = new HashSet<(string Type, string Id)>();
|
||||
var blob = GetBlobInfo(tx);
|
||||
db.WalletObjects.Add(new Data.WalletObjectData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
Type = Data.WalletObjectData.Types.Tx,
|
||||
Id = tx.TransactionId,
|
||||
Data = string.IsNullOrEmpty(blob.Comment) ? null : new JObject() { ["comment"] = blob.Comment }.ToString()
|
||||
});
|
||||
|
||||
foreach (var label in blob.Labels)
|
||||
{
|
||||
var labelId = label.Key;
|
||||
if (labelId.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
labelId = JObject.Parse(label.Key)["value"].Value<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
if (!existingLabels.Contains((tx.WalletDataId, labelId)))
|
||||
{
|
||||
JObject labelData = new JObject();
|
||||
labelData.Add("color", "#000");
|
||||
db.WalletObjects.Add(new WalletObjectData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
Type = WalletObjectData.Types.Label,
|
||||
Id = labelId,
|
||||
Data = labelData.ToString()
|
||||
});
|
||||
existingLabels.Add((tx.WalletDataId, labelId));
|
||||
}
|
||||
if (ifuckinghateentityframework2.Add((Data.WalletObjectData.Types.Label, labelId)))
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = Data.WalletObjectData.Types.Label,
|
||||
ParentId = labelId
|
||||
});
|
||||
|
||||
if (label.Value is ReferenceLabel reflabel)
|
||||
{
|
||||
if (IsReferenceLabel(reflabel.Type))
|
||||
{
|
||||
if (ifuckinghateentityframework.Add((tx.WalletDataId, reflabel.Type, reflabel.Reference ?? String.Empty)))
|
||||
db.WalletObjects.Add(new WalletObjectData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
Type = reflabel.Type,
|
||||
Id = reflabel.Reference ?? String.Empty
|
||||
});
|
||||
|
||||
if (ifuckinghateentityframework2.Add((reflabel.Type, reflabel.Reference ?? String.Empty)))
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = reflabel.Type,
|
||||
ParentId = reflabel.Reference ?? String.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (label.Value is PayoutLabel payoutLabel)
|
||||
{
|
||||
foreach (var pp in payoutLabel.PullPaymentPayouts)
|
||||
{
|
||||
foreach (var payout in pp.Value)
|
||||
{
|
||||
var payoutData = string.IsNullOrEmpty(pp.Key) ? null : new JObject()
|
||||
{
|
||||
["pullPaymentId"] = pp.Key
|
||||
};
|
||||
if (ifuckinghateentityframework.Add((tx.WalletDataId, "payout", payout)))
|
||||
db.WalletObjects.Add(new WalletObjectData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
Type = "payout",
|
||||
Id = payout,
|
||||
Data = payoutData?.ToString()
|
||||
});
|
||||
if (ifuckinghateentityframework2.Add(("payout", payout)))
|
||||
db.WalletObjectLinks.Add(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = tx.WalletDataId,
|
||||
ChildType = Data.WalletObjectData.Types.Tx,
|
||||
ChildId = tx.TransactionId,
|
||||
ParentType = "payout",
|
||||
ParentId = payout
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int retry = 0;
|
||||
retrySave:
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException ex) when (retry < 10)
|
||||
{
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
if (entry.Entity is WalletObjectData wo && (IsReferenceLabel(wo.Type) || wo.Type == "payout"))
|
||||
{
|
||||
await entry.ReloadAsync();
|
||||
}
|
||||
}
|
||||
retry++;
|
||||
goto retrySave;
|
||||
}
|
||||
if (txs.Length < batchCount)
|
||||
{
|
||||
var settings = await _settingsRepository.GetSettingAsync<MigrationSettings>();
|
||||
settings.MigratedTransactionLabels = int.MaxValue;
|
||||
await _settingsRepository.UpdateSetting(settings);
|
||||
Logs.PayServer.LogInformation($"Wallet transaction label successfully migrated");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
startFromOffset += batchCount;
|
||||
var settings = await _settingsRepository.GetSettingAsync<MigrationSettings>();
|
||||
settings.MigratedTransactionLabels = startFromOffset;
|
||||
await _settingsRepository.UpdateSetting(settings);
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReferenceLabel(string type)
|
||||
{
|
||||
return type == "invoice" ||
|
||||
type == "payment-request" ||
|
||||
type == "app" ||
|
||||
type == "pj-exposed";
|
||||
}
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
private async Task MigratedInvoiceTextSearchToDb(int startFromPage)
|
||||
{
|
||||
// deleting legacy DBriize database if present
|
||||
|
@ -97,7 +327,7 @@ namespace BTCPayServer.HostedServices
|
|||
textSearch.Add(invoice.RefundMail);
|
||||
// TODO: Are there more things to cache? PaymentData?
|
||||
InvoiceRepository.AddToTextSearch(ctx,
|
||||
new InvoiceData { Id = invoice.Id, InvoiceSearchData = new List<InvoiceSearchData>() },
|
||||
new Data.InvoiceData { Id = invoice.Id, InvoiceSearchData = new List<InvoiceSearchData>() },
|
||||
textSearch.ToArray());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
@ -14,6 +15,8 @@ using BTCPayServer.Services.Apps;
|
|||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
@ -32,7 +35,6 @@ namespace BTCPayServer.HostedServices
|
|||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<UpdateTransactionLabel>();
|
||||
}
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -42,136 +44,21 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
|
||||
var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
|
||||
var labels = new List<(string color, Label label)>
|
||||
var labels = new List<Attachment>
|
||||
{
|
||||
UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id)
|
||||
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
||||
};
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(UpdateTransactionLabel.PaymentRequestLabelTemplate(paymentId));
|
||||
labels.Add(Attachment.PaymentRequest(paymentId));
|
||||
}
|
||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
labels.Add(UpdateTransactionLabel.AppLabelTemplate(appId));
|
||||
labels.Add(Attachment.App(appId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(walletId, transactionId, labels));
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||
}
|
||||
else if (evt is UpdateTransactionLabel updateTransactionLabel)
|
||||
{
|
||||
var walletTransactionsInfo =
|
||||
await _walletRepository.GetWalletTransactionsInfo(updateTransactionLabel.WalletId);
|
||||
var walletBlobInfo = await _walletRepository.GetWalletInfo(updateTransactionLabel.WalletId);
|
||||
await Task.WhenAll(updateTransactionLabel.TransactionLabels.Select(async pair =>
|
||||
{
|
||||
var txId = pair.Key.ToString();
|
||||
var coloredLabels = pair.Value;
|
||||
if (!walletTransactionsInfo.TryGetValue(txId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
|
||||
bool walletNeedUpdate = false;
|
||||
foreach (var cl in coloredLabels)
|
||||
{
|
||||
if (walletBlobInfo.LabelColors.TryGetValue(cl.label.Text, out var currentColor))
|
||||
{
|
||||
if (currentColor != cl.color)
|
||||
{
|
||||
walletNeedUpdate = true;
|
||||
walletBlobInfo.LabelColors[cl.label.Text] = currentColor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
walletNeedUpdate = true;
|
||||
walletBlobInfo.LabelColors.AddOrReplace(cl.label.Text, cl.color);
|
||||
}
|
||||
}
|
||||
|
||||
if (walletNeedUpdate)
|
||||
await _walletRepository.SetWalletInfo(updateTransactionLabel.WalletId, walletBlobInfo);
|
||||
foreach (var cl in coloredLabels)
|
||||
{
|
||||
var label = cl.label;
|
||||
if (walletTransactionInfo.Labels.TryGetValue(label.Text, out var existingLabel))
|
||||
{
|
||||
label = label.Merge(existingLabel);
|
||||
}
|
||||
|
||||
walletTransactionInfo.Labels.AddOrReplace(label.Text, label);
|
||||
}
|
||||
|
||||
await _walletRepository.SetWalletTransactionInfo(updateTransactionLabel.WalletId,
|
||||
txId, walletTransactionInfo);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateTransactionLabel
|
||||
{
|
||||
public UpdateTransactionLabel()
|
||||
{
|
||||
|
||||
}
|
||||
public UpdateTransactionLabel(WalletId walletId, uint256 txId, (string color, Label label) colorLabel)
|
||||
{
|
||||
WalletId = walletId;
|
||||
TransactionLabels = new Dictionary<uint256, List<(string color, Label label)>>();
|
||||
TransactionLabels.Add(txId, new List<(string color, Label label)>() { colorLabel });
|
||||
}
|
||||
public UpdateTransactionLabel(WalletId walletId, uint256 txId, List<(string color, Label label)> colorLabels)
|
||||
{
|
||||
WalletId = walletId;
|
||||
TransactionLabels = new Dictionary<uint256, List<(string color, Label label)>>();
|
||||
TransactionLabels.Add(txId, colorLabels);
|
||||
}
|
||||
public static (string color, Label label) PayjoinLabelTemplate()
|
||||
{
|
||||
return ("#51b13e", new RawLabel("payjoin"));
|
||||
}
|
||||
|
||||
public static (string color, Label label) InvoiceLabelTemplate(string invoice)
|
||||
{
|
||||
return ("#cedc21", new ReferenceLabel("invoice", invoice));
|
||||
}
|
||||
public static (string color, Label label) PaymentRequestLabelTemplate(string paymentRequestId)
|
||||
{
|
||||
return ("#489D77", new ReferenceLabel("payment-request", paymentRequestId));
|
||||
}
|
||||
public static (string color, Label label) AppLabelTemplate(string appId)
|
||||
{
|
||||
return ("#5093B6", new ReferenceLabel("app", appId));
|
||||
}
|
||||
|
||||
public static (string color, Label label) PayjoinExposedLabelTemplate(string invoice)
|
||||
{
|
||||
return ("#51b13e", new ReferenceLabel("pj-exposed", invoice));
|
||||
}
|
||||
|
||||
public static (string color, Label label) PayoutTemplate(Dictionary<string, List<string>> pullPaymentToPayouts, string walletId)
|
||||
{
|
||||
return ("#3F88AF", new PayoutLabel()
|
||||
{
|
||||
PullPaymentPayouts = pullPaymentToPayouts,
|
||||
WalletId = walletId
|
||||
});
|
||||
}
|
||||
public WalletId WalletId { get; set; }
|
||||
public Dictionary<uint256, List<(string color, Label label)>> TransactionLabels { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach (var transactionLabel in TransactionLabels)
|
||||
{
|
||||
result.AppendLine(CultureInfo.InvariantCulture,
|
||||
$"Adding {transactionLabel.Value.Count} labels to {transactionLabel.Key} in wallet {WalletId}");
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,6 @@ namespace BTCPayServer.Hosting
|
|||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<ISettingsRepository>(provider => provider.GetService<SettingsRepository>());
|
||||
services.TryAddSingleton<IStoreRepository>(provider => provider.GetService<StoreRepository>());
|
||||
services.TryAddSingleton<LabelFactory>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
|
||||
services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>();
|
||||
|
|
|
@ -28,6 +28,7 @@ using Microsoft.Extensions.Options;
|
|||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PeterO.Cbor;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
|
@ -215,6 +216,12 @@ namespace BTCPayServer.Hosting
|
|||
settings.MigrateEmailServerDisableTLSCerts = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.MigrateWalletColors)
|
||||
{
|
||||
await MigrateMigrateLabels();
|
||||
settings.MigrateWalletColors = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -223,6 +230,59 @@ namespace BTCPayServer.Hosting
|
|||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|
||||
static WalletBlobInfo GetBlobInfo(WalletData walletData)
|
||||
{
|
||||
if (walletData.Blob == null || walletData.Blob.Length == 0)
|
||||
{
|
||||
return new WalletBlobInfo();
|
||||
}
|
||||
var blobInfo = JsonConvert.DeserializeObject<WalletBlobInfo>(ZipUtils.Unzip(walletData.Blob));
|
||||
return blobInfo;
|
||||
}
|
||||
|
||||
private async Task MigrateMigrateLabels()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
var wallets = await ctx.Wallets.AsNoTracking().ToArrayAsync();
|
||||
foreach (var wallet in wallets)
|
||||
{
|
||||
var blob = GetBlobInfo(wallet);
|
||||
HashSet<string> labels = new HashSet<string>(blob.LabelColors.Count);
|
||||
foreach (var label in blob.LabelColors)
|
||||
{
|
||||
var labelId = label.Key;
|
||||
if (labelId.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
labelId = JObject.Parse(label.Key)["value"].Value<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
if (!labels.Add(labelId))
|
||||
continue;
|
||||
var obj = new JObject();
|
||||
obj.Add("color", label.Value);
|
||||
var labelObjId = new WalletObjectId(WalletId.Parse(wallet.Id),
|
||||
WalletObjectData.Types.Label,
|
||||
labelId);
|
||||
ctx.WalletObjects.Add(new WalletObjectData()
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
Type = WalletObjectData.Types.Label,
|
||||
Id = labelId,
|
||||
Data = obj.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
// In the past, if a server was considered local network, then we would disable TLS checks.
|
||||
// Now we don't do it anymore, as we have an explicit flag (DisableCertificateCheck) to control the behavior.
|
||||
// But we need to migrate old users that relied on the behavior before.
|
||||
|
|
|
@ -15,9 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public string Link { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public HashSet<ColoredLabel> Labels { get; set; } = new HashSet<ColoredLabel>();
|
||||
public HashSet<TransactionTagModel> Tags { get; set; } = new HashSet<TransactionTagModel>();
|
||||
}
|
||||
public HashSet<ColoredLabel> Labels { get; set; } = new HashSet<ColoredLabel>();
|
||||
public HashSet<(string Text, string Color, string TextColor)> Labels { get; set; } = new HashSet<(string Text, string Color, string TextColor)>();
|
||||
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
|
||||
public override int CurrentPageCount => Transactions.Count;
|
||||
public string CryptoCode { get; set; }
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace BTCPayServer.Services.Labels
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class ColoredLabel
|
||||
public class TransactionTagModel
|
||||
{
|
||||
internal ColoredLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public string Text { get; internal set; }
|
||||
public string Color { get; internal set; }
|
||||
public string TextColor { get; internal set; }
|
||||
public string Link { get; internal set; }
|
||||
public string Tooltip { get; internal set; }
|
||||
public string Tooltip { get; internal set; } = String.Empty;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
ColoredLabel item = obj as ColoredLabel;
|
||||
TransactionTagModel item = obj as TransactionTagModel;
|
||||
if (item == null)
|
||||
return false;
|
||||
return Text.Equals(item.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool operator ==(ColoredLabel a, ColoredLabel b)
|
||||
public static bool operator ==(TransactionTagModel a, TransactionTagModel b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
|
@ -31,7 +27,7 @@ namespace BTCPayServer.Services.Labels
|
|||
return a.Text == b.Text;
|
||||
}
|
||||
|
||||
public static bool operator !=(ColoredLabel a, ColoredLabel b)
|
||||
public static bool operator !=(TransactionTagModel a, TransactionTagModel b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
|
@ -73,7 +73,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
|
||||
public class InputSelectionOption
|
||||
{
|
||||
public IEnumerable<ColoredLabel> Labels { get; set; }
|
||||
public IEnumerable<TransactionTagModel> Labels { get; set; }
|
||||
public string Comment { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Outpoint { get; set; }
|
||||
|
|
|
@ -81,6 +81,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
}
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private readonly BTCPayWalletProvider _btcPayWalletProvider;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
|
@ -96,6 +97,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
|
||||
public PayJoinEndpointController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository, ExplorerClientProvider explorerClientProvider,
|
||||
WalletRepository walletRepository,
|
||||
BTCPayWalletProvider btcPayWalletProvider,
|
||||
UTXOLocker utxoLocker,
|
||||
EventAggregator eventAggregator,
|
||||
|
@ -109,6 +111,7 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_walletRepository = walletRepository;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_btcPayWalletProvider = btcPayWalletProvider;
|
||||
_utxoLocker = utxoLocker;
|
||||
|
@ -503,23 +506,14 @@ namespace BTCPayServer.Payments.PayJoin
|
|||
}
|
||||
|
||||
await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction);
|
||||
var labels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
|
||||
new KeyValuePair<uint256, List<(string color, Label label)>>(utxo.Key,
|
||||
new List<(string color, Label label)>()
|
||||
{
|
||||
UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice?.Id)
|
||||
}))
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
|
||||
labels.Add(originalPaymentData.PayjoinInformation.CoinjoinTransactionHash, new List<(string color, Label label)>()
|
||||
foreach (var utxo in selectedUTXOs)
|
||||
{
|
||||
UpdateTransactionLabel.PayjoinLabelTemplate()
|
||||
});
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel()
|
||||
{
|
||||
WalletId = walletId,
|
||||
TransactionLabels = labels
|
||||
});
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, utxo.Key.Hash, Attachment.PayjoinExposed(invoice?.Id));
|
||||
}
|
||||
await _walletRepository.AddWalletTransactionAttachment(walletId, originalPaymentData.PayjoinInformation.CoinjoinTransactionHash, Attachment.Payjoin());
|
||||
|
||||
|
||||
ctx.Success();
|
||||
// BTCPay Server support PSBT set as hex
|
||||
if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
|
||||
|
|
|
@ -39,6 +39,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
ILoggerFactory logger,
|
||||
BitcoinLikePayoutHandler bitcoinLikePayoutHandler,
|
||||
EventAggregator eventAggregator,
|
||||
WalletRepository walletRepository,
|
||||
StoreRepository storeRepository,
|
||||
PayoutProcessorData payoutProcesserSettings,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
|
@ -51,8 +52,11 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_bitcoinLikePayoutHandler = bitcoinLikePayoutHandler;
|
||||
_eventAggregator = eventAggregator;
|
||||
WalletRepository = walletRepository;
|
||||
}
|
||||
|
||||
public WalletRepository WalletRepository { get; }
|
||||
|
||||
protected override async Task Process(ISupportedPaymentMethod paymentMethod, List<PayoutData> payouts)
|
||||
{
|
||||
var storePaymentMethod = paymentMethod as DerivationSchemeSettings;
|
||||
|
@ -171,12 +175,9 @@ namespace BTCPayServer.PayoutProcessors.OnChain
|
|||
var walletId = new WalletId(_PayoutProcesserSettings.StoreId, PaymentMethodId.CryptoCode);
|
||||
foreach (PayoutData payoutData in transfersProcessing)
|
||||
{
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(walletId,
|
||||
await WalletRepository.AddWalletTransactionAttachment(walletId,
|
||||
txHash,
|
||||
UpdateTransactionLabel.PayoutTemplate(new ()
|
||||
{
|
||||
{payoutData.PullPaymentDataId?? "", new List<string>{payoutData.Id}}
|
||||
}, walletId.ToString())));
|
||||
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id));
|
||||
}
|
||||
await Task.WhenAny(tcs.Task, task);
|
||||
}
|
||||
|
|
59
BTCPayServer/Services/Attachment.cs
Normal file
59
BTCPayServer/Services/Attachment.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Attachment
|
||||
{
|
||||
public string Type { get; }
|
||||
public string Id { get; }
|
||||
public JObject? Data { get; }
|
||||
|
||||
public Attachment(string type, string? id = null, JObject? data = null)
|
||||
{
|
||||
Type = type;
|
||||
Id = id ?? string.Empty;
|
||||
Data = data;
|
||||
}
|
||||
public static Attachment Payjoin()
|
||||
{
|
||||
return new Attachment("payjoin");
|
||||
}
|
||||
public static Attachment Invoice(string invoice)
|
||||
{
|
||||
return new Attachment("invoice", invoice);
|
||||
}
|
||||
public static Attachment PaymentRequest(string paymentRequestId)
|
||||
{
|
||||
return new Attachment("payment-request", paymentRequestId);
|
||||
}
|
||||
public static Attachment App(string appId)
|
||||
{
|
||||
return new Attachment("app", appId);
|
||||
}
|
||||
|
||||
public static Attachment PayjoinExposed(string? invoice)
|
||||
{
|
||||
return new Attachment("pj-exposed", invoice);
|
||||
}
|
||||
|
||||
public static IEnumerable<Attachment> Payout(string? pullPaymentId, string payoutId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pullPaymentId))
|
||||
{
|
||||
yield return new Attachment("payout", payoutId);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Attachment("payout", payoutId, new JObject()
|
||||
{
|
||||
["pullPaymentId"] = pullPaymentId
|
||||
});
|
||||
yield return new Attachment("pull-payment", pullPaymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,13 +9,9 @@ using Newtonsoft.Json.Linq;
|
|||
namespace BTCPayServer.Services.Labels
|
||||
{
|
||||
|
||||
[Obsolete]
|
||||
public abstract class Label : LabelData
|
||||
{
|
||||
public virtual Label Merge(LabelData other)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
static void FixLegacy(JObject jObj, ReferenceLabel refLabel)
|
||||
{
|
||||
if (refLabel.Reference is null && jObj.ContainsKey("id"))
|
||||
|
@ -49,6 +45,18 @@ namespace BTCPayServer.Services.Labels
|
|||
rawLabel.Type = "raw";
|
||||
FixLegacy(jObj, (Label)rawLabel);
|
||||
}
|
||||
|
||||
static Label()
|
||||
{
|
||||
SerializerSettings = new JsonSerializerSettings()
|
||||
{
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
Serializer = JsonSerializer.Create(SerializerSettings);
|
||||
}
|
||||
|
||||
public static JsonSerializerSettings SerializerSettings;
|
||||
public static JsonSerializer Serializer;
|
||||
public static Label Parse(string str)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(str);
|
||||
|
@ -95,6 +103,7 @@ namespace BTCPayServer.Services.Labels
|
|||
}
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public class RawLabel : Label
|
||||
{
|
||||
public RawLabel()
|
||||
|
@ -106,6 +115,7 @@ namespace BTCPayServer.Services.Labels
|
|||
Text = text;
|
||||
}
|
||||
}
|
||||
[Obsolete]
|
||||
public class ReferenceLabel : Label
|
||||
{
|
||||
public ReferenceLabel()
|
||||
|
@ -121,6 +131,7 @@ namespace BTCPayServer.Services.Labels
|
|||
[JsonProperty("ref")]
|
||||
public string Reference { get; set; }
|
||||
}
|
||||
[Obsolete]
|
||||
public class PayoutLabel : Label
|
||||
{
|
||||
public PayoutLabel()
|
||||
|
@ -130,21 +141,5 @@ namespace BTCPayServer.Services.Labels
|
|||
}
|
||||
|
||||
public Dictionary<string, List<string>> PullPaymentPayouts { get; set; } = new();
|
||||
public string WalletId { get; set; }
|
||||
|
||||
public override Label Merge(LabelData other)
|
||||
{
|
||||
if (other is not PayoutLabel otherPayoutLabel) return base.Merge(other);
|
||||
foreach (var pullPaymentPayout in otherPayoutLabel.PullPaymentPayouts)
|
||||
{
|
||||
if (!PullPaymentPayouts.TryGetValue(pullPaymentPayout.Key, out var pullPaymentPayouts))
|
||||
{
|
||||
pullPaymentPayouts = new List<string>();
|
||||
PullPaymentPayouts.Add(pullPaymentPayout.Key, pullPaymentPayouts);
|
||||
}
|
||||
pullPaymentPayouts.AddRange(pullPaymentPayout.Value);
|
||||
}
|
||||
return base.Merge(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,197 +0,0 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
namespace BTCPayServer.Services.Labels
|
||||
{
|
||||
public class LabelFactory
|
||||
{
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
|
||||
public LabelFactory(
|
||||
LinkGenerator linkGenerator,
|
||||
WalletRepository walletRepository
|
||||
)
|
||||
{
|
||||
_linkGenerator = linkGenerator;
|
||||
_walletRepository = walletRepository;
|
||||
}
|
||||
|
||||
public IEnumerable<ColoredLabel> ColorizeTransactionLabels(WalletBlobInfo walletBlobInfo, WalletTransactionInfo transactionInfo,
|
||||
HttpRequest request)
|
||||
{
|
||||
foreach (var label in transactionInfo.Labels)
|
||||
{
|
||||
walletBlobInfo.LabelColors.TryGetValue(label.Value.Text, out var color);
|
||||
yield return CreateLabel(label.Value, color, request);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ColoredLabel> GetWalletColoredLabels(WalletBlobInfo walletBlobInfo, HttpRequest request)
|
||||
{
|
||||
foreach (var kv in walletBlobInfo.LabelColors)
|
||||
{
|
||||
yield return CreateLabel(new RawLabel() { Text = kv.Key }, kv.Value, request);
|
||||
}
|
||||
}
|
||||
|
||||
const string DefaultColor = "#000";
|
||||
private ColoredLabel CreateLabel(LabelData uncoloredLabel, string? color, HttpRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uncoloredLabel);
|
||||
color ??= DefaultColor;
|
||||
|
||||
ColoredLabel coloredLabel = new ColoredLabel
|
||||
{
|
||||
Text = uncoloredLabel.Text,
|
||||
Color = color,
|
||||
Tooltip = "",
|
||||
TextColor = TextColor(color)
|
||||
};
|
||||
|
||||
string PayoutLabelText(KeyValuePair<string, List<string>>? pair = null)
|
||||
{
|
||||
if (pair is null)
|
||||
{
|
||||
return "Paid a payout";
|
||||
}
|
||||
return pair.Value.Value.Count == 1 ? $"Paid a payout {(string.IsNullOrEmpty(pair.Value.Key)? string.Empty: $"of a pull payment ({pair.Value.Key})")}" : $"Paid {pair.Value.Value.Count} payouts {(string.IsNullOrEmpty(pair.Value.Key)? string.Empty: $"of a pull payment ({pair.Value.Key})")}";
|
||||
}
|
||||
|
||||
if (uncoloredLabel is ReferenceLabel refLabel)
|
||||
{
|
||||
var refInLabel = string.IsNullOrEmpty(refLabel.Reference) ? string.Empty : $"({refLabel.Reference})";
|
||||
switch (uncoloredLabel.Type)
|
||||
{
|
||||
case "invoice":
|
||||
coloredLabel.Tooltip = $"Received through an invoice {refInLabel}";
|
||||
coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
|
||||
? null
|
||||
: _linkGenerator.InvoiceLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
|
||||
break;
|
||||
case "payment-request":
|
||||
coloredLabel.Tooltip = $"Received through a payment request {refInLabel}";
|
||||
coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
|
||||
? null
|
||||
: _linkGenerator.PaymentRequestLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
|
||||
break;
|
||||
case "app":
|
||||
coloredLabel.Tooltip = $"Received through an app {refInLabel}";
|
||||
coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
|
||||
? null
|
||||
: _linkGenerator.AppLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
|
||||
break;
|
||||
case "pj-exposed":
|
||||
coloredLabel.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice {refInLabel}";
|
||||
coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
|
||||
? null
|
||||
: _linkGenerator.InvoiceLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (uncoloredLabel is PayoutLabel payoutLabel)
|
||||
{
|
||||
coloredLabel.Tooltip = payoutLabel.PullPaymentPayouts?.Count switch
|
||||
{
|
||||
null => PayoutLabelText(),
|
||||
0 => PayoutLabelText(),
|
||||
1 => PayoutLabelText(payoutLabel.PullPaymentPayouts.First()),
|
||||
_ =>
|
||||
$"<ul>{string.Join(string.Empty, payoutLabel.PullPaymentPayouts.Select(pair => $"<li>{PayoutLabelText(pair)}</li>"))}</ul>"
|
||||
};
|
||||
|
||||
coloredLabel.Link = string.IsNullOrEmpty(payoutLabel.WalletId)
|
||||
? null
|
||||
: _linkGenerator.PayoutLink(payoutLabel.WalletId, null, PayoutState.Completed, request.Scheme, request.Host,
|
||||
request.PathBase);
|
||||
}
|
||||
else if (uncoloredLabel.Text == "payjoin")
|
||||
{
|
||||
coloredLabel.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
return coloredLabel;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
readonly string[] LabelColorScheme =
|
||||
{
|
||||
"#fbca04",
|
||||
"#0e8a16",
|
||||
"#ff7619",
|
||||
"#84b6eb",
|
||||
"#5319e7",
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
};
|
||||
|
||||
readonly int MaxLabelSize = 20;
|
||||
|
||||
async public Task<RawLabel> BuildLabel(
|
||||
WalletBlobInfo walletBlobInfo,
|
||||
HttpRequest request,
|
||||
WalletTransactionInfo walletTransactionInfo,
|
||||
WalletId walletId,
|
||||
string transactionId,
|
||||
string label
|
||||
)
|
||||
{
|
||||
label = label.Trim().TrimStart('{').ToLowerInvariant().Replace(',', ' ').Truncate(MaxLabelSize);
|
||||
var labels = GetWalletColoredLabels(walletBlobInfo, request);
|
||||
|
||||
if (!labels.Any(l => l.Text.Equals(label, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var chosenColor = ChooseBackgroundColor(walletBlobInfo, request);
|
||||
walletBlobInfo.LabelColors.Add(label, chosenColor);
|
||||
await _walletRepository.SetWalletInfo(walletId, walletBlobInfo);
|
||||
}
|
||||
|
||||
return new RawLabel(label);
|
||||
}
|
||||
|
||||
private string ChooseBackgroundColor(
|
||||
WalletBlobInfo walletBlobInfo,
|
||||
HttpRequest request
|
||||
)
|
||||
{
|
||||
var labels = GetWalletColoredLabels(walletBlobInfo, request);
|
||||
|
||||
List<string> allColors = new List<string>();
|
||||
allColors.AddRange(LabelColorScheme);
|
||||
allColors.AddRange(labels.Select(l => l.Color));
|
||||
var chosenColor =
|
||||
allColors
|
||||
.GroupBy(k => k)
|
||||
.OrderBy(k => k.Count())
|
||||
.ThenBy(k =>
|
||||
{
|
||||
var indexInColorScheme = Array.IndexOf(LabelColorScheme, k.Key);
|
||||
|
||||
// Ensures that any label color which may not be in our label color scheme is given the least priority
|
||||
return indexInColorScheme == -1 ? double.PositiveInfinity : indexInColorScheme;
|
||||
})
|
||||
.First().Key;
|
||||
|
||||
return chosenColor;
|
||||
}
|
||||
|
||||
private string TextColor(string bgColor)
|
||||
{
|
||||
int nThreshold = 105;
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ namespace BTCPayServer.Services
|
|||
|
||||
// Done in DbMigrationsHostedService
|
||||
public int? MigratedInvoiceTextSearchPages { get; set; }
|
||||
public int? MigratedTransactionLabels { get; set; }
|
||||
public bool MigrateAppCustomOption { get; set; }
|
||||
public bool MigratePayoutDestinationId { get; set; }
|
||||
public bool AddInitialUserBlob { get; set; }
|
||||
|
@ -32,5 +33,6 @@ namespace BTCPayServer.Services
|
|||
public bool LighingAddressDatabaseMigration { get; set; }
|
||||
public bool AddStoreToPayout { get; set; }
|
||||
public bool MigrateEmailServerDisableTLSCerts { get; set; }
|
||||
public bool MigrateWalletColors { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
#nullable enable
|
||||
public record WalletObjectId(WalletId WalletId, string Type, string Id);
|
||||
#nullable restore
|
||||
public class WalletRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
|
@ -16,74 +26,256 @@ namespace BTCPayServer.Services
|
|||
_ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
|
||||
}
|
||||
|
||||
public async Task SetWalletInfo(WalletId walletId, WalletBlobInfo blob)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var walletData = new WalletData() { Id = walletId.ToString() };
|
||||
walletData.SetBlobInfo(blob);
|
||||
var entity = await ctx.Wallets.AddAsync(walletData);
|
||||
entity.State = EntityState.Modified;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Does not exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, WalletTransactionInfo>> GetWalletTransactionsInfo(WalletId walletId, string[] transactionIds = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
return (await ctx.WalletTransactions
|
||||
.Where(w => w.WalletDataId == walletId.ToString())
|
||||
.Where(data => transactionIds == null || transactionIds.Contains(data.TransactionId))
|
||||
.Select(w => w)
|
||||
|
||||
|
||||
IQueryable<WalletObjectLinkData> wols;
|
||||
IQueryable<WalletObjectData> wos;
|
||||
|
||||
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
|
||||
// Such request isn't well optimized by postgres, and create different requests clogging up
|
||||
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
|
||||
if (ctx.Database.IsNpgsql() && transactionIds is not null)
|
||||
{
|
||||
wos = ctx.WalletObjects
|
||||
.FromSqlInterpolated($"SELECT wos.* FROM unnest({transactionIds}) t JOIN \"WalletObjects\" wos ON wos.\"WalletId\"={walletId.ToString()} AND wos.\"Type\"={WalletObjectData.Types.Tx} AND wos.\"Id\"=t")
|
||||
.AsNoTracking();
|
||||
wols = ctx.WalletObjectLinks
|
||||
.FromSqlInterpolated($"SELECT wol.* FROM unnest({transactionIds}) t JOIN \"WalletObjectLinks\" wol ON wol.\"WalletId\"={walletId.ToString()} AND wol.\"ChildType\"={WalletObjectData.Types.Tx} AND wol.\"ChildId\"=t")
|
||||
.AsNoTracking();
|
||||
}
|
||||
else // Unefficient path
|
||||
{
|
||||
wos = ctx.WalletObjects
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Tx && (transactionIds == null || transactionIds.Contains(w.Id)));
|
||||
wols = ctx.WalletObjectLinks
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.ChildType == WalletObjectData.Types.Tx && (transactionIds == null || transactionIds.Contains(w.ChildId)));
|
||||
}
|
||||
var links = await wols
|
||||
.Select(tx =>
|
||||
new
|
||||
{
|
||||
TxId = tx.ChildId,
|
||||
AssociatedDataId = tx.ParentId,
|
||||
AssociatedDataType = tx.ParentType,
|
||||
AssociatedData = tx.Parent.Data
|
||||
})
|
||||
.ToArrayAsync();
|
||||
var objs = await wos
|
||||
.Select(tx =>
|
||||
new
|
||||
{
|
||||
TxId = tx.Id,
|
||||
Data = tx.Data
|
||||
})
|
||||
.ToArrayAsync();
|
||||
|
||||
var result = new Dictionary<string, WalletTransactionInfo>(objs.Length);
|
||||
foreach (var obj in objs)
|
||||
{
|
||||
var data = obj.Data is null ? null : JObject.Parse(obj.Data);
|
||||
result.Add(obj.TxId, new WalletTransactionInfo(walletId)
|
||||
{
|
||||
Comment = data?["comment"]?.Value<string>()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
foreach (var row in links)
|
||||
{
|
||||
JObject data = row.AssociatedData is null ? null : JObject.Parse(row.AssociatedData);
|
||||
var info = result[row.TxId];
|
||||
|
||||
if (row.AssociatedDataType == WalletObjectData.Types.Label)
|
||||
{
|
||||
info.LabelColors.TryAdd(row.AssociatedDataId, data["color"]?.Value<string>() ?? "#000");
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Attachments.Add(new Attachment(row.AssociatedDataType, row.AssociatedDataId, row.AssociatedData is null ? null : JObject.Parse(row.AssociatedData)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
|
||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletId walletId)
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
return (await ctx.WalletObjects
|
||||
.AsNoTracking()
|
||||
.Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Label)
|
||||
.Select(o => new { o.Id, o.Data })
|
||||
.ToArrayAsync())
|
||||
.ToDictionary(w => w.TransactionId, w => w.GetBlobInfo());
|
||||
.Select(o => (o.Id, JObject.Parse(o.Data)["color"]!.Value<string>()!))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<WalletBlobInfo> GetWalletInfo(WalletId walletId)
|
||||
public async Task EnsureWalletObjectLink(WalletObjectId parent, WalletObjectId child)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var data = await ctx.Wallets
|
||||
.Where(w => w.Id == walletId.ToString())
|
||||
.Select(w => w)
|
||||
.FirstOrDefaultAsync();
|
||||
return data?.GetBlobInfo() ?? new WalletBlobInfo();
|
||||
}
|
||||
|
||||
public async Task SetWalletTransactionInfo(WalletId walletId, string transactionId, WalletTransactionInfo walletTransactionInfo)
|
||||
var l = new WalletObjectLinkData()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
ArgumentNullException.ThrowIfNull(transactionId);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var walletData = new WalletTransactionData() { WalletDataId = walletId.ToString(), TransactionId = transactionId };
|
||||
walletData.SetBlobInfo(walletTransactionInfo);
|
||||
var entity = await ctx.WalletTransactions.AddAsync(walletData);
|
||||
entity.State = EntityState.Modified;
|
||||
WalletId = parent.WalletId.ToString(),
|
||||
ChildType = child.Type,
|
||||
ChildId = child.Id,
|
||||
ParentType = parent.Type,
|
||||
ParentId = parent.Id
|
||||
};
|
||||
ctx.WalletObjectLinks.Add(l);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // Does not exists
|
||||
catch (DbUpdateException) // already exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
}
|
||||
}
|
||||
|
||||
public static int MaxCommentSize = 200;
|
||||
public async Task SetWalletObjectComment(WalletObjectId id, string comment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
ArgumentNullException.ThrowIfNull(comment);
|
||||
if (!string.IsNullOrEmpty(comment))
|
||||
await ModifyWalletObjectData(id, (o) => o["comment"] = comment.Trim().Truncate(MaxCommentSize));
|
||||
else
|
||||
await ModifyWalletObjectData(id, (o) => o.Remove("comment"));
|
||||
}
|
||||
|
||||
|
||||
static WalletObjectData NewWalletObjectData(WalletObjectId id, JObject? data = null)
|
||||
{
|
||||
return new WalletObjectData()
|
||||
{
|
||||
WalletId = id.WalletId.ToString(),
|
||||
Type = id.Type,
|
||||
Id = id.Id,
|
||||
Data = data?.ToString()
|
||||
};
|
||||
}
|
||||
public async Task ModifyWalletObjectData(WalletObjectId id, Action<JObject> modify)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
ArgumentNullException.ThrowIfNull(modify);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var obj = await ctx.WalletObjects.FindAsync(id.WalletId.ToString(), id.Type, id.Id);
|
||||
if (obj is null)
|
||||
{
|
||||
obj = NewWalletObjectData(id);
|
||||
ctx.WalletObjects.Add(obj);
|
||||
}
|
||||
var currentData = obj.Data is null ? new JObject() : JObject.Parse(obj.Data);
|
||||
modify(currentData);
|
||||
obj.Data = currentData.ToString();
|
||||
if (obj.Data == "{}")
|
||||
obj.Data = null;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
const int MaxLabelSize = 50;
|
||||
public async Task AddWalletObjectLabels(WalletObjectId id, params string[] labels)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
await EnsureWalletObject(id);
|
||||
foreach (var l in labels.Select(l => l.Trim().Truncate(MaxLabelSize)))
|
||||
{
|
||||
var labelObjId = new WalletObjectId(id.WalletId, WalletObjectData.Types.Label, l);
|
||||
await EnsureWalletObject(labelObjId, new JObject()
|
||||
{
|
||||
["color"] = ColorPalette.Default.DeterministicColor(l)
|
||||
});
|
||||
await EnsureWalletObjectLink(labelObjId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, Attachment attachment)
|
||||
{
|
||||
return AddWalletTransactionAttachment(walletId, txId, new[] { attachment });
|
||||
}
|
||||
public async Task AddWalletTransactionAttachment(WalletId walletId, uint256 txId, IEnumerable<Attachment> attachments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walletId);
|
||||
ArgumentNullException.ThrowIfNull(txId);
|
||||
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, txId.ToString());
|
||||
await EnsureWalletObject(txObjId);
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
var labelObjId = new WalletObjectId(walletId, WalletObjectData.Types.Label, attachment.Type);
|
||||
await EnsureWalletObject(labelObjId, new JObject()
|
||||
{
|
||||
["color"] = ColorPalette.Default.DeterministicColor(attachment.Type)
|
||||
});
|
||||
await EnsureWalletObjectLink(labelObjId, txObjId);
|
||||
if (attachment.Data is not null || attachment.Id.Length != 0)
|
||||
{
|
||||
var data = new WalletObjectId(walletId, attachment.Type, attachment.Id);
|
||||
await EnsureWalletObject(data, attachment.Data);
|
||||
await EnsureWalletObjectLink(data, txObjId);
|
||||
}
|
||||
}
|
||||
}
|
||||
public async Task RemoveWalletObjectLabels(WalletObjectId id, params string[] labels)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
foreach (var l in labels.Select(l => l.Trim()))
|
||||
{
|
||||
var labelObjId = new WalletObjectId(id.WalletId, WalletObjectData.Types.Label, l);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjectLinks.Remove(new WalletObjectLinkData()
|
||||
{
|
||||
WalletId = id.WalletId.ToString(),
|
||||
ChildId = id.Id,
|
||||
ChildType = id.Type,
|
||||
ParentId = labelObjId.Id,
|
||||
ParentType = labelObjId.Type
|
||||
});
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // the Wallet does not exists in the DB
|
||||
catch (DbUpdateException) // Already deleted, do nothing
|
||||
{
|
||||
await SetWalletInfo(walletId, new WalletBlobInfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetWalletObject(WalletObjectId id, JObject? data)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
var o = NewWalletObjectData(id, data);
|
||||
ctx.WalletObjects.Add(o);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // already exists
|
||||
{
|
||||
ctx.Entry(o).State = EntityState.Modified;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureWalletObject(WalletObjectId id, JObject? data = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
ctx.WalletObjects.Add(NewWalletObjectData(id, data));
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateException) // already exists
|
||||
{
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Wallets.Export
|
|||
|
||||
if (_walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
|
||||
{
|
||||
model.Labels = transactionInfo.Labels?.Select(l => l.Value.Text).ToList();
|
||||
model.Labels = transactionInfo.LabelColors?.Select(l => l.Key).ToList();
|
||||
model.Comment = transactionInfo.Comment;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
@transaction.Timestamp.ToBrowserDate()
|
||||
</td>
|
||||
<td class="text-start">
|
||||
@if (transaction.Labels.Any())
|
||||
@if (transaction.Tags.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in transaction.Labels)
|
||||
@foreach (var label in transaction.Tags)
|
||||
{
|
||||
<div class="badge-container rounded-2" style="background-color:@label.Color;">
|
||||
<div class="badge transactionLabel position-relative d-block"
|
||||
|
@ -79,7 +79,7 @@
|
|||
<div class="py-2 px-3 d-flex flex-wrap gap-2">
|
||||
@foreach (var label in Model.Labels)
|
||||
{
|
||||
@if (transaction.Labels.Contains(label))
|
||||
@if (transaction.Tags.Any(l => l.Text == label.Text))
|
||||
{
|
||||
<button name="removelabel" class="bg-transparent border-0 p-0" type="submit" value="@label.Text"><span class="badge" style="background-color:@label.Color;color:@label.TextColor"><span class="fa fa-check"></span> @label.Text</span></button>
|
||||
}
|
||||
|
|
|
@ -708,6 +708,7 @@
|
|||
"LabelData": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"deprecated": true,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
|
@ -762,6 +763,7 @@
|
|||
},
|
||||
"labels": {
|
||||
"description": "Labels linked to this transaction",
|
||||
"deprecated": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LabelData"
|
||||
|
@ -810,6 +812,7 @@
|
|||
},
|
||||
"labels": {
|
||||
"description": "Labels linked to this transaction",
|
||||
"deprecated": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LabelData"
|
||||
|
@ -904,6 +907,7 @@
|
|||
},
|
||||
"labels": {
|
||||
"nullable": true,
|
||||
"deprecated": true,
|
||||
"description": "Transaction labels",
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
Loading…
Add table
Reference in a new issue