From 37f84592f41be36295619ecb564459c536b2c0d9 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 22 Mar 2024 12:59:12 +0000 Subject: [PATCH] Store fee --- .../Configuration/PaymentsConfiguration.cs | 4 +- NostrStreamer/Database/Payment.cs | 7 +- .../20240322105026_PaymentFee.Designer.cs | 469 ++++++++++++++++++ .../Migrations/20240322105026_PaymentFee.cs | 31 ++ .../StreamerContextModelSnapshot.cs | 3 + .../Services/Background/LndInvoiceStream.cs | 2 +- NostrStreamer/Services/UserService.cs | 12 +- 7 files changed, 519 insertions(+), 9 deletions(-) create mode 100644 NostrStreamer/Migrations/20240322105026_PaymentFee.Designer.cs create mode 100644 NostrStreamer/Migrations/20240322105026_PaymentFee.cs diff --git a/NostrStreamer/Database/Configuration/PaymentsConfiguration.cs b/NostrStreamer/Database/Configuration/PaymentsConfiguration.cs index bfb9f0f..509a188 100644 --- a/NostrStreamer/Database/Configuration/PaymentsConfiguration.cs +++ b/NostrStreamer/Database/Configuration/PaymentsConfiguration.cs @@ -22,9 +22,11 @@ public class PaymentsConfiguration : IEntityTypeConfiguration builder.Property(a => a.Nostr); builder.Property(a => a.Type) .IsRequired(); + builder.Property(a => a.Fee) + .IsRequired(); builder.HasOne(a => a.User) .WithMany(a => a.Payments) .HasForeignKey(a => a.PubKey); } -} +} \ No newline at end of file diff --git a/NostrStreamer/Database/Payment.cs b/NostrStreamer/Database/Payment.cs index 47f7827..5c20ae0 100644 --- a/NostrStreamer/Database/Payment.cs +++ b/NostrStreamer/Database/Payment.cs @@ -12,7 +12,7 @@ public class Payment public bool IsPaid { get; set; } /// - /// Payment amount in sats!! + /// Payment amount in milli-sats!! /// public ulong Amount { get; init; } @@ -21,6 +21,11 @@ public class Payment public string? Nostr { get; init; } public PaymentType Type { get; init; } + + /// + /// Fee paid for withdrawal in milli-sats + /// + public ulong Fee { get; init; } } public enum PaymentType diff --git a/NostrStreamer/Migrations/20240322105026_PaymentFee.Designer.cs b/NostrStreamer/Migrations/20240322105026_PaymentFee.Designer.cs new file mode 100644 index 0000000..83fb3c8 --- /dev/null +++ b/NostrStreamer/Migrations/20240322105026_PaymentFee.Designer.cs @@ -0,0 +1,469 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NostrStreamer.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + [DbContext(typeof(StreamerContext))] + [Migration("20240322105026_PaymentFee")] + partial class PaymentFee + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NostrStreamer.Database.IngestEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("App") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Capabilities") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("Forward") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("App") + .IsUnique(); + + b.ToTable("Endpoints"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.Property("PaymentHash") + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric(20,0)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Fee") + .HasColumnType("numeric(20,0)"); + + b.Property("Invoice") + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("Nostr") + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("PaymentHash"); + + b.HasIndex("PubKey"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone"); + + b.Property("Pubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SubscriberPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TargetPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TargetPubkey"); + + b.HasIndex("SubscriberPubkey", "TargetPubkey") + .IsUnique(); + + b.ToTable("PushSubscriptionTargets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Property("PubKey") + .HasColumnType("text"); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("Goal") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("TosAccepted") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("PubKey"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EdgeIp") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndpointId") + .HasColumnType("uuid"); + + b.Property("Ends") + .HasColumnType("timestamp with time zone"); + + b.Property("Event") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForwardClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastSegment") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("numeric"); + + b.Property("MilliSatsCollected") + .HasColumnType("numeric"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Starts") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StreamId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId"); + + b.HasIndex("PubKey"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TakenByPubkey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Clips"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserPubkey"); + + b.ToTable("Forwards"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Relay") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("Sig") + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("ZapSplit") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("StreamId", "PubKey") + .IsUnique(); + + b.ToTable("Guests"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Recordings"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Payments") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Streams") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Forwards") + .HasForeignKey("UserPubkey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Guests") + .HasForeignKey("StreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Recordings") + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Navigation("Forwards"); + + b.Navigation("Payments"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20240322105026_PaymentFee.cs b/NostrStreamer/Migrations/20240322105026_PaymentFee.cs new file mode 100644 index 0000000..5518ecb --- /dev/null +++ b/NostrStreamer/Migrations/20240322105026_PaymentFee.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class PaymentFee : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Fee", + table: "Payments", + type: "numeric(20,0)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.Sql("update \"Payments\" set \"Amount\" = \"Amount\" * 1000"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Fee", + table: "Payments"); + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index 76a4363..878b14c 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -67,6 +67,9 @@ namespace NostrStreamer.Migrations b.Property("Created") .HasColumnType("timestamp with time zone"); + b.Property("Fee") + .HasColumnType("numeric(20,0)"); + b.Property("Invoice") .HasColumnType("text"); diff --git a/NostrStreamer/Services/Background/LndInvoiceStream.cs b/NostrStreamer/Services/Background/LndInvoiceStream.cs index d06b006..ab1213e 100644 --- a/NostrStreamer/Services/Background/LndInvoiceStream.cs +++ b/NostrStreamer/Services/Background/LndInvoiceStream.cs @@ -55,7 +55,7 @@ public class LndInvoicesStream : BackgroundService if (payment is {IsPaid: false} && msg.State is Invoice.Types.InvoiceState.Settled) { payment.IsPaid = true; - payment.User.Balance += (long)(payment.Amount * 1000L); + payment.User.Balance += (long)payment.Amount; await db.SaveChangesAsync(stoppingToken); if (!string.IsNullOrEmpty(payment.Nostr) && !string.IsNullOrEmpty(payment.Invoice)) { diff --git a/NostrStreamer/Services/UserService.cs b/NostrStreamer/Services/UserService.cs index 0777555..52146b2 100644 --- a/NostrStreamer/Services/UserService.cs +++ b/NostrStreamer/Services/UserService.cs @@ -43,7 +43,7 @@ public class UserService PubKey = pubkey, Type = PaymentType.Credit, IsPaid = true, - Amount = (ulong)user.Balance / 1000, + Amount = (ulong)user.Balance, PaymentHash = SHA256.HashData(Encoding.UTF8.GetBytes($"{pubkey}-init-credit")).ToHex() }); @@ -69,7 +69,7 @@ public class UserService _db.Payments.Add(new() { PubKey = pubkey, - Amount = amount / 1000, + Amount = amount, Invoice = invoice.PaymentRequest, PaymentHash = invoice.RHash.ToByteArray().ToHex(), Nostr = nostr, @@ -111,7 +111,7 @@ public class UserService Invoice = invoice, Type = PaymentType.Withdrawal, PaymentHash = rHash, - Amount = (ulong)pr.MinimumAmount.MilliSatoshi / 1000, + Amount = (ulong)pr.MinimumAmount.MilliSatoshi }); await _db.SaveChangesAsync(); @@ -128,8 +128,8 @@ public class UserService await _db.Payments .Where(a => a.PaymentHash == rHash) .ExecuteUpdateAsync(o => o.SetProperty(v => v.IsPaid, true) - .SetProperty(v => v.Amount, b => b.Amount + (ulong)result.FeeSat)); - + .SetProperty(v => v.Fee, (ulong)result.FeeMsat)); + // take fee from balance await _db.Users .Where(a => a.PubKey == pubkey) @@ -158,7 +158,7 @@ public class UserService public async Task MaxWithdrawalAmount(string pubkey) { - var credit = 1000 * await _db.Payments + var credit = await _db.Payments .Where(a => a.PubKey == pubkey && a.IsPaid && a.Type == PaymentType.Credit)