Files
BTCPayServerPlugins/Plugins/BTCPayServer.Plugins.SideShift/SideShiftController.cs
2023-07-18 11:23:06 +02:00

315 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using Newtonsoft.Json.Linq;
using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes;
namespace BTCPayServer.Plugins.SideShift
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/{storeId}/SideShift")]
public class SideShiftController : Controller
{
private readonly SideShiftService _sideShiftService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly ApplicationDbContextFactory _dbContextFactory;
public SideShiftController(
SideShiftService sideShiftService,
IHttpClientFactory httpClientFactory,
IEnumerable<IPayoutHandler> payoutHandlers,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings serializerSettings, ApplicationDbContextFactory dbContextFactory)
{
_sideShiftService = sideShiftService;
_httpClientFactory = httpClientFactory;
_payoutHandlers = payoutHandlers;
_pullPaymentHostedService = pullPaymentHostedService;
_serializerSettings = serializerSettings;
_dbContextFactory = dbContextFactory;
}
[HttpGet("")]
public async Task<IActionResult> UpdateSideShiftSettings(string storeId)
{
SideShiftSettings SideShift = null;
try
{
SideShift = await _sideShiftService.GetSideShiftForStore(storeId);
}
catch (Exception)
{
// ignored
}
return View(SideShift??new SideShiftSettings());
}
[HttpPost("")]
public async Task<IActionResult> UpdateSideShiftSettings(string storeId, SideShiftSettings vm,
string command)
{
if (vm.Enabled)
{
if (!ModelState.IsValid)
{
return View(vm);
}
}
switch (command)
{
case "save":
await _sideShiftService.SetSideShiftForStore(storeId, vm);
TempData["SuccessMessage"] = "SideShift settings modified";
return RedirectToAction(nameof(UpdateSideShiftSettings), new {storeId});
default:
return View(vm);
}
}
[HttpPost("~/plugins/sidehift/{pullPaymentId}/payouts")]
[AllowAnonymous]
public async Task<IActionResult> CreatePayout(string pullPaymentId,
[FromBody] CreateSideShiftPayoutRequest request)
{
IPayoutHandler handler = null;
if (string.IsNullOrEmpty(request.ShiftCurrency))
{
ModelState.AddModelError(nameof(request.ShiftCurrency), "ShiftCurrency must be specified");
}
if (string.IsNullOrEmpty(request.ShiftNetwork))
{
ModelState.AddModelError(nameof(request.ShiftNetwork), "ShiftNetwork must be specified");
}
if (request.Amount is null)
{
ModelState.AddModelError(nameof(request.Amount), "Amount must be specified");
}
if (!PaymentMethodId.TryParse(request.PaymentMethod, out var pmi))
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
}
else
{
handler = _payoutHandlers.FindPayoutHandler(pmi);
if (handler == null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
}
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var pp = await
_pullPaymentHostedService.GetPullPayment(pullPaymentId, false);
var ppBlob = pp?.GetBlob();
if (ppBlob is null)
{
return NotFound();
}
var ip = HttpContext.Connection.RemoteIpAddress;
var client = _httpClientFactory.CreateClient("sideshift");
if (ip is not null && !ip.IsLocal())
client.DefaultRequestHeaders.Add("x-user-ip", ip.ToString());
//
// var quoteResponse = await client.PostAsJsonAsync("https://sideshift.ai/api/v2/quotes", new
// {
// depositCoin = pmi.CryptoCode,
// depositNetwork = pmi.PaymentType == LightningPaymentType.Instance ? "lightning" : null,
// settleCoin = request.ShiftCurrency,
// settleNetwork = request.ShiftNetwork,
// depositAmount = request.Amount.ToString(),
// affiliateId = "qg0OrfHJV"
// }
// );
// quoteResponse.EnsureSuccessStatusCode();
// var quote = await quoteResponse.Content.ReadAsAsync<QuoteResponse>();
// var shiftResponse = await client.PostAsJsonAsync("https://sideshift.ai/api/v2/shifts/fixed", new
// {
// settleAddress = request.Destination,
// settleMemo = request.Memo,
// quoteId = quote.id,
// affiliateId = "qg0OrfHJV"
// }
// );
// shiftResponse.EnsureSuccessStatusCode();
// var shift = await shiftResponse.Content.ReadAsAsync<ShiftResponse>();
var shiftResponse = await client.PostAsJsonAsync("https://sideshift.ai/api/v2/shifts/variable", new
{
settleAddress = request.Destination,
affiliateId = "qg0OrfHJV",
settleMemo = request.Memo,
depositCoin = pmi.CryptoCode,
depositNetwork = pmi.PaymentType == LightningPaymentType.Instance ? "lightning" : null,
settleCoin = request.ShiftCurrency,
settleNetwork = request.ShiftNetwork,
}
);
if (!shiftResponse.IsSuccessStatusCode)
{
var error = JObject.Parse(await shiftResponse.Content.ReadAsStringAsync());
ModelState.AddModelError("",error["error"]["message"].Value<string>());
return this.CreateValidationError(ModelState);
}
var shift = await shiftResponse.Content.ReadAsAsync<ShiftResponse>();
var destination =
await handler.ParseAndValidateClaimDestination(pmi, shift.depositAddress, ppBlob,
CancellationToken.None);
var claim = await _pullPaymentHostedService.Claim(new ClaimRequest()
{
PullPaymentId = pullPaymentId,
Destination = destination.destination,
PaymentMethodId = pmi,
Value = request.Amount
});
if (claim.Result == ClaimRequest.ClaimResult.Ok)
{
await using var ctx = _dbContextFactory.CreateContext();
ppBlob.Description += $"<br/>The payout of {claim.PayoutData.Destination} will be forwarded to SideShift.ai for further conversion. Please go to <a href=\"https://sideshift.ai/orders/{shift.id}?openSupport=true\">the order page</a> for support.";
pp.SetBlob(ppBlob);
ctx.Attach(pp).State = EntityState.Modified;
await ctx.SaveChangesAsync();
}
return HandleClaimResult(claim);
}
private IActionResult HandleClaimResult(ClaimRequest.ClaimResponse result)
{
switch (result.Result)
{
case ClaimRequest.ClaimResult.Ok:
break;
case ClaimRequest.ClaimResult.Duplicate:
return this.CreateAPIError("duplicate-destination", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.Expired:
return this.CreateAPIError("expired", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.NotStarted:
return this.CreateAPIError("not-started", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.Archived:
return this.CreateAPIError("archived", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.Overdraft:
return this.CreateAPIError("overdraft", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.AmountTooLow:
return this.CreateAPIError("amount-too-low", ClaimRequest.GetErrorMessage(result.Result));
case ClaimRequest.ClaimResult.PaymentMethodNotSupported:
return this.CreateAPIError("payment-method-not-supported",
ClaimRequest.GetErrorMessage(result.Result));
default:
throw new NotSupportedException("Unsupported ClaimResult");
}
return Ok(ToModel(result.PayoutData));
}
private Client.Models.PayoutData ToModel(Data.PayoutData p)
{
var blob = p.GetBlob(_serializerSettings);
var model = new Client.Models.PayoutData
{
Id = p.Id,
PullPaymentId = p.PullPaymentDataId,
Date = p.Date,
Amount = blob.Amount,
PaymentMethodAmount = blob.CryptoAmount,
Revision = blob.Revision,
State = p.State,
Destination = blob.Destination,
PaymentMethod = p.PaymentMethodId,
CryptoCode = p.GetPaymentMethodId().CryptoCode,
PaymentProof = p.GetProofBlobJson()
};
return model;
}
public class CreateSideShiftPayoutThroughStoreRequest : CreatePayoutThroughStoreRequest
{
public string Memo { get; set; }
public string ShiftCurrency { get; set; }
public string ShiftNetwork { get; set; }
}
public class CreateSideShiftPayoutRequest : CreatePayoutRequest
{
public string Memo { get; set; }
public string ShiftCurrency { get; set; }
public string ShiftNetwork { get; set; }
}
public class QuoteResponse
{
public string id { get; set; }
public string createdAt { get; set; }
public string depositCoin { get; set; }
public string settleCoin { get; set; }
public string depositNetwork { get; set; }
public string settleNetwork { get; set; }
public string expiresAt { get; set; }
public string depositAmount { get; set; }
public string settleAmount { get; set; }
public string rate { get; set; }
public string affiliateId { get; set; }
}
public class ShiftResponse
{
public string id { get; set; }
public string createdAt { get; set; }
public string depositCoin { get; set; }
public string settleCoin { get; set; }
public string depositNetwork { get; set; }
public string settleNetwork { get; set; }
public string depositAddress { get; set; }
public string settleAddress { get; set; }
public string depositMin { get; set; }
public string depositMax { get; set; }
public string refundAddress { get; set; }
public string type { get; set; }
public string quoteId { get; set; }
public string depositAmount { get; set; }
public string settleAmount { get; set; }
public string expiresAt { get; set; }
public string status { get; set; }
public string updatedAt { get; set; }
public string rate { get; set; }
public string averageShiftSeconds { get; set; }
}
}
}