feat: withdrawal

This commit is contained in:
2024-03-22 10:07:30 +00:00
parent 5d2a4c4360
commit 805a92537c
4 changed files with 128 additions and 7 deletions

View File

@ -28,7 +28,8 @@ public class NostrController : Controller
private readonly ILogger<NostrController> _logger;
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)
{
_db = db;
@ -185,7 +186,8 @@ public class NostrController : Controller
}
[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)
{
var pk = GetPubKey();
@ -265,7 +267,7 @@ public class NostrController : Controller
var existing = await _db.PushSubscriptions.FirstOrDefaultAsync(a => a.Key == sub.Key);
if (existing != default)
{
return Json(new {id = existing.Id});
return Json(new { id = existing.Id });
}
var newId = Guid.NewGuid();
@ -296,7 +298,7 @@ public class NostrController : Controller
var sub = await _db.PushSubscriptionTargets
.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)
.Select(a => a.TargetPubkey)
.ToListAsync();
@ -351,6 +353,22 @@ public class NostrController : Controller
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()
{
var pk = GetPubKey();

View File

@ -11,6 +11,9 @@ public class Payment
public bool IsPaid { get; set; }
/// <summary>
/// Payment amount in sats!!
/// </summary>
public ulong Amount { get; init; }
public DateTime Created { get; init; } = DateTime.UtcNow;
@ -24,5 +27,6 @@ public enum PaymentType
{
Topup = 0,
Zap = 1,
Credit = 2
Credit = 2,
Withdrawal = 3,
}

View File

@ -67,4 +67,17 @@ public class RecordingDeleter : BackgroundService
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);
}
}

View File

@ -1,7 +1,9 @@
using System.Security.Cryptography;
using System.Text;
using BTCPayServer.Lightning;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Nostr.Client.Utils;
using NostrStreamer.ApiModel;
using NostrStreamer.Database;
@ -79,6 +81,90 @@ public class UserService
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)
{
return await _db.Users.AsNoTracking()