feat: withdrawal
This commit is contained in:
@ -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();
|
||||
@ -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();
|
||||
|
@ -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,
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
Reference in New Issue
Block a user