feat: withdrawal
This commit is contained in:
@ -28,7 +28,8 @@ public class NostrController : Controller
|
|||||||
private readonly ILogger<NostrController> _logger;
|
private readonly ILogger<NostrController> _logger;
|
||||||
private readonly PushSender _pushSender;
|
private readonly PushSender _pushSender;
|
||||||
|
|
||||||
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager, UserService userService,
|
public NostrController(StreamerContext db, Config config, StreamManagerFactory streamManager,
|
||||||
|
UserService userService,
|
||||||
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender)
|
IClipService clipService, ILogger<NostrController> logger, PushSender pushSender)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@ -185,7 +186,8 @@ public class NostrController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("clip/{streamId:guid}/{tempClipId:guid}")]
|
[HttpPost("clip/{streamId:guid}/{tempClipId:guid}")]
|
||||||
public async Task<IActionResult> MakeClip([FromRoute] Guid streamId, [FromRoute] Guid tempClipId, [FromQuery] float start,
|
public async Task<IActionResult> MakeClip([FromRoute] Guid streamId, [FromRoute] Guid tempClipId,
|
||||||
|
[FromQuery] float start,
|
||||||
[FromQuery] float length)
|
[FromQuery] float length)
|
||||||
{
|
{
|
||||||
var pk = GetPubKey();
|
var pk = GetPubKey();
|
||||||
@ -265,7 +267,7 @@ public class NostrController : Controller
|
|||||||
var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key);
|
var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key);
|
||||||
if (existing != default)
|
if (existing != default)
|
||||||
{
|
{
|
||||||
return Json(new {id = existing.Id});
|
return Json(new { id = existing.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
var newId = Guid.NewGuid();
|
var newId = Guid.NewGuid();
|
||||||
@ -296,7 +298,7 @@ public class NostrController : Controller
|
|||||||
|
|
||||||
var sub = await _db.PushSubscriptionTargets
|
var sub = await _db.PushSubscriptionTargets
|
||||||
.Join(_db.PushSubscriptions, a => a.SubscriberPubkey, b => b.Pubkey,
|
.Join(_db.PushSubscriptions, a => a.SubscriberPubkey, b => b.Pubkey,
|
||||||
(a, b) => new {a.SubscriberPubkey, a.TargetPubkey, b.Auth})
|
(a, b) => new { a.SubscriberPubkey, a.TargetPubkey, b.Auth })
|
||||||
.Where(a => a.SubscriberPubkey == userPubkey && a.Auth == auth)
|
.Where(a => a.SubscriberPubkey == userPubkey && a.Auth == auth)
|
||||||
.Select(a => a.TargetPubkey)
|
.Select(a => a.TargetPubkey)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@ -351,6 +353,22 @@ public class NostrController : Controller
|
|||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("withdraw")]
|
||||||
|
public async Task<IActionResult> WithdrawFunds([FromQuery] string invoice)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(invoice)) return BadRequest();
|
||||||
|
|
||||||
|
var userPubkey = GetPubKey();
|
||||||
|
if (string.IsNullOrEmpty(userPubkey))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var (fee, preimage) = await _userService.WithdrawFunds(userPubkey, invoice);
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
fee, preimage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<User?> GetUser()
|
private async Task<User?> GetUser()
|
||||||
{
|
{
|
||||||
var pk = GetPubKey();
|
var pk = GetPubKey();
|
||||||
@ -362,4 +380,4 @@ public class NostrController : Controller
|
|||||||
var claim = HttpContext.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.Name);
|
var claim = HttpContext.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.Name);
|
||||||
return claim!.Value;
|
return claim!.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,6 +11,9 @@ public class Payment
|
|||||||
|
|
||||||
public bool IsPaid { get; set; }
|
public bool IsPaid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Payment amount in sats!!
|
||||||
|
/// </summary>
|
||||||
public ulong Amount { get; init; }
|
public ulong Amount { get; init; }
|
||||||
|
|
||||||
public DateTime Created { get; init; } = DateTime.UtcNow;
|
public DateTime Created { get; init; } = DateTime.UtcNow;
|
||||||
@ -24,5 +27,6 @@ public enum PaymentType
|
|||||||
{
|
{
|
||||||
Topup = 0,
|
Topup = 0,
|
||||||
Zap = 1,
|
Zap = 1,
|
||||||
Credit = 2
|
Credit = 2,
|
||||||
|
Withdrawal = 3,
|
||||||
}
|
}
|
@ -67,4 +67,17 @@ public class RecordingDeleter : BackgroundService
|
|||||||
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
|
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DeleteBroken(StreamerContext db, IDvrStore dvrStore, CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var olderThan = DateTime.UtcNow.Subtract(TimeSpan.FromDays(_config.RetainRecordingsDays));
|
||||||
|
|
||||||
|
var toDelete = await db.Streams
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.Starts < olderThan)
|
||||||
|
.Select(a => a.Id)
|
||||||
|
.ToListAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting (broken) delete of {n:###0} stream recordings", toDelete.Count);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,9 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NBitcoin;
|
||||||
using Nostr.Client.Utils;
|
using Nostr.Client.Utils;
|
||||||
using NostrStreamer.ApiModel;
|
using NostrStreamer.ApiModel;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
@ -79,6 +81,90 @@ public class UserService
|
|||||||
return invoice.PaymentRequest;
|
return invoice.PaymentRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(long Fee, string Preimage)> WithdrawFunds(string pubkey, string invoice)
|
||||||
|
{
|
||||||
|
var user = await GetUser(pubkey);
|
||||||
|
if (user == default) throw new Exception("No user found");
|
||||||
|
|
||||||
|
var maxOut = await MaxWithdrawalAmount(pubkey);
|
||||||
|
var pr = BOLT11PaymentRequest.Parse(invoice, invoice.StartsWith("lnbc1") ? Network.Main : Network.RegTest);
|
||||||
|
if (pr.MinimumAmount == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("0 amount invoice not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxOut <= pr.MinimumAmount.MilliSatoshi)
|
||||||
|
{
|
||||||
|
throw new Exception("Not enough balance to pay invoice");
|
||||||
|
}
|
||||||
|
|
||||||
|
// start by taking balance
|
||||||
|
var rHash = pr.PaymentHash!.ToString();
|
||||||
|
await using (var tx = await _db.Database.BeginTransactionAsync())
|
||||||
|
{
|
||||||
|
await _db.Users
|
||||||
|
.Where(a => a.PubKey == pubkey)
|
||||||
|
.ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance - pr.MinimumAmount.MilliSatoshi));
|
||||||
|
_db.Payments.Add(new()
|
||||||
|
{
|
||||||
|
PubKey = pubkey,
|
||||||
|
Invoice = invoice,
|
||||||
|
Type = PaymentType.Withdrawal,
|
||||||
|
PaymentHash = rHash,
|
||||||
|
Amount = (ulong)pr.MinimumAmount.MilliSatoshi / 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const double feeMax = 0.005; // 0.5% max fee
|
||||||
|
var result = await _lnd.SendPayment(invoice, (long)(pr.MinimumAmount.MilliSatoshi * feeMax));
|
||||||
|
if (result?.Status is Lnrpc.Payment.Types.PaymentStatus.Succeeded)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
return (result.FeeMsat, result.PaymentPreimage);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Payment failed");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// return balance on error
|
||||||
|
await _db.Users
|
||||||
|
.Where(a => a.PubKey == pubkey)
|
||||||
|
.ExecuteUpdateAsync(p => p.SetProperty(o => o.Balance, b => b.Balance + pr.MinimumAmount.MilliSatoshi));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Payment>> ListPayments(string pubkey)
|
||||||
|
{
|
||||||
|
return await _db.Payments
|
||||||
|
.Where(a => a.PubKey == pubkey && a.IsPaid)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> MaxWithdrawalAmount(string pubkey)
|
||||||
|
{
|
||||||
|
var credit = 1000 * await _db.Payments
|
||||||
|
.Where(a => a.PubKey == pubkey &&
|
||||||
|
a.IsPaid &&
|
||||||
|
a.Type == PaymentType.Credit)
|
||||||
|
.SumAsync(a => (long)a.Amount);
|
||||||
|
|
||||||
|
var balance = await _db.Users
|
||||||
|
.Where(a => a.PubKey == pubkey)
|
||||||
|
.Select(a => a.Balance)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
return Math.Max(0, balance - credit);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<User?> GetUser(string pubkey)
|
public async Task<User?> GetUser(string pubkey)
|
||||||
{
|
{
|
||||||
return await _db.Users.AsNoTracking()
|
return await _db.Users.AsNoTracking()
|
||||||
@ -124,4 +210,4 @@ public class UserService
|
|||||||
.SetProperty(v => v.ContentWarning, req.ContentWarning)
|
.SetProperty(v => v.ContentWarning, req.ContentWarning)
|
||||||
.SetProperty(v => v.Goal, req.Goal));
|
.SetProperty(v => v.Goal, req.Goal));
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user