mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
pimp ss and prism plugins
This commit is contained in:
@@ -1,20 +1,16 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace BTCPayServer.Plugins.Prism;
|
||||
|
||||
@@ -24,10 +20,12 @@ namespace BTCPayServer.Plugins.Prism;
|
||||
public class PrismController : Controller
|
||||
{
|
||||
private readonly SatBreaker _satBreaker;
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
|
||||
public PrismController( SatBreaker satBreaker)
|
||||
public PrismController( SatBreaker satBreaker, IPluginHookService pluginHookService)
|
||||
{
|
||||
_satBreaker = satBreaker;
|
||||
_pluginHookService = pluginHookService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -40,7 +38,8 @@ public class PrismController : Controller
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Edit(string storeId, PrismSettings settings, string command)
|
||||
{
|
||||
for (int i = 0; i < settings.Splits?.Length; i++)
|
||||
|
||||
for (var i = 0; i < settings.Splits?.Length; i++)
|
||||
{
|
||||
var prism = settings.Splits[i];
|
||||
if (string.IsNullOrEmpty(prism.Source))
|
||||
@@ -77,6 +76,7 @@ public class PrismController : Controller
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
LNURL.LNURL.ExtractUriFromInternetIdentifier(dest);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -87,7 +87,8 @@ public class PrismController : Controller
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
||||
var result = await _pluginHookService.ApplyFilter("prism-destination-validate", dest);
|
||||
if(result is not true)
|
||||
ModelState.AddModelError($"Splits[{i}].Destinations[{j}].Destination", "Destination is not a valid LN address or LNURL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public class PrismPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.9.0"}
|
||||
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.10.0"}
|
||||
};
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
@@ -17,6 +18,7 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
|
||||
|
||||
namespace BTCPayServer.Plugins.Prism
|
||||
{
|
||||
@@ -34,6 +36,7 @@ namespace BTCPayServer.Plugins.Prism
|
||||
private readonly LightningClientFactoryService _lightningClientFactoryService;
|
||||
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly IPluginHookService _pluginHookService;
|
||||
private Dictionary<string, PrismSettings> _prismSettings;
|
||||
|
||||
public SatBreaker(StoreRepository storeRepository,
|
||||
@@ -45,7 +48,8 @@ namespace BTCPayServer.Plugins.Prism
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
LightningClientFactoryService lightningClientFactoryService,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) : base(eventAggregator, logger)
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
|
||||
IPluginHookService pluginHookService) : base(eventAggregator, logger)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_logger = logger;
|
||||
@@ -56,6 +60,7 @@ namespace BTCPayServer.Plugins.Prism
|
||||
_lightningClientFactoryService = lightningClientFactoryService;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
_pluginHookService = pluginHookService;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -286,22 +291,43 @@ namespace BTCPayServer.Plugins.Prism
|
||||
new[] {InvoiceEventCode.Completed, InvoiceEventCode.MarkedCompleted}.Contains(
|
||||
invoiceEvent.EventCode))
|
||||
{
|
||||
|
||||
|
||||
if (!_prismSettings.TryGetValue(invoiceEvent.Invoice.StoreId, out var prismSettings) || !prismSettings.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var catchAllPrism = prismSettings.Splits.FirstOrDefault(split => split.Source == "*");
|
||||
Split prism = null;
|
||||
LightningAddressData address = null;
|
||||
var pm = invoiceEvent.Invoice.GetPaymentMethod(new PaymentMethodId("BTC", LNURLPayPaymentType.Instance));
|
||||
var pmd = pm?.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
|
||||
if (string.IsNullOrEmpty(pmd?.ConsumedLightningAddress))
|
||||
var pmdRaw = pm?.GetPaymentMethodDetails();
|
||||
var pmd = pmdRaw as LNURLPayPaymentMethodDetails;
|
||||
if (string.IsNullOrEmpty(pmd?.ConsumedLightningAddress) && catchAllPrism is null)
|
||||
{
|
||||
return;
|
||||
}else if (!string.IsNullOrEmpty(pmd?.ConsumedLightningAddress))
|
||||
{
|
||||
address = await _lightningAddressService.ResolveByAddress(pmd.ConsumedLightningAddress.Split("@")[0]);
|
||||
if (address is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var address =
|
||||
await _lightningAddressService.ResolveByAddress(pmd.ConsumedLightningAddress.Split("@")[0]);
|
||||
if (address is null || !_prismSettings.TryGetValue(address.StoreDataId, out var prismSettings) ||
|
||||
!prismSettings.Enabled)
|
||||
{
|
||||
return;
|
||||
prism = prismSettings.Splits.FirstOrDefault(s => s.Source.Equals(address.Username, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
var splits = prismSettings.Splits.FirstOrDefault(s => s.Source == address.Username)?.Destinations;
|
||||
else
|
||||
{
|
||||
//do not run prism on payments not LN based.
|
||||
if (invoiceEvent.Invoice.GetPayments("BTC", true).All(entity =>
|
||||
{
|
||||
var pmi = entity.GetPaymentMethodId();
|
||||
return pmi.CryptoCode == "BTC" && (pmi.PaymentType == LightningPaymentType.Instance || pmi.PaymentType == LNURLPayPaymentType.Instance);
|
||||
}))
|
||||
prism = catchAllPrism;
|
||||
}
|
||||
var splits = prism?.Destinations;
|
||||
if (splits?.Any() is not true)
|
||||
{
|
||||
return;
|
||||
@@ -357,10 +383,23 @@ namespace BTCPayServer.Plugins.Prism
|
||||
{
|
||||
continue;
|
||||
}
|
||||
IClaimDestination dest = null;
|
||||
var dest2 = await _pluginHookService.ApplyFilter("prism-claim-destination", destination);
|
||||
|
||||
dest = dest2 switch
|
||||
{
|
||||
IClaimDestination claimDestination => claimDestination,
|
||||
string destStr when !string.IsNullOrEmpty(destStr) => new LNURLPayClaimDestinaton(destStr),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (dest is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var payout = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
{
|
||||
Destination = new LNURLPayClaimDestinaton(destination),
|
||||
Destination = dest,
|
||||
PreApprove = true,
|
||||
StoreId = storeId,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", LightningPaymentType.Instance),
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
@if (!users.Any())
|
||||
{
|
||||
<div class="alert alert-warning mb-5" role="alert">
|
||||
Prisms can currently only work on lightning addresses that are owned by the store. Please create a lightning address for the store.
|
||||
<a class="alert-link p-0" asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">Configure now</a>
|
||||
Prisms can currently mostly work on lightning addresses that are owned by the store. Please create a lightning address for the store.
|
||||
<a class="alert-link p-0" asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">Configure now</a>. <br/><br/>Alternatively, you can use * as the source, which will match any settled invoice as long as it was paid through Lightning.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</a>
|
||||
</h2>
|
||||
<p class="text-muted">
|
||||
The prism plugin allows automated value splits for your lightning payments. You can set up multiple prisms, each with their own source (which is a <a asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">lightning address username</a>) and destinations (which are other lightning addresses or lnurls). The plugin will automatically credit the configured percentage of the payment to the destination (while also making sure there is 2% reserved to cater for fees, don't worry, once the lightning node tells us the exact fee amount, we credit/debit the balance after the payment), and once the configured threshold is reached, a <a asp-action="Payouts" asp-controller="UIStorePullPayments" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()" asp-route-payoutState="AwaitingPayment" asp-route-paymentMethodId="@pmi.ToString()">payout</a> will be created. Then, a <a asp-action="ConfigureStorePayoutProcessors" asp-controller="UIPayoutProcessors" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">payout processor</a> will run at intervals and process the payout.
|
||||
The prism plugin allows automated value splits for your lightning payments. You can set up multiple prisms, each with their own source (which is a <a asp-action="EditLightningAddress" asp-controller="UILNURL" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">lightning address username</a>, or use * as a catch-all for all invoices settled through Lightning, excluding ones which Prism can handle explicitly) and destinations (which are other lightning addresses or lnurls). The plugin will automatically credit the configured percentage of the payment to the destination (while also making sure there is 2% reserved to cater for fees, don't worry, once the lightning node tells us the exact fee amount, we credit/debit the balance after the payment), and once the configured threshold is reached, a <a asp-action="Payouts" asp-controller="UIStorePullPayments" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()" asp-route-payoutState="AwaitingPayment" asp-route-paymentMethodId="@pmi.ToString()">payout</a> will be created. Then, a <a asp-action="ConfigureStorePayoutProcessors" asp-controller="UIPayoutProcessors" asp-route-storeId="@ScopeProvider.GetCurrentStoreId()">payout processor</a> will run at intervals and process the payout.
|
||||
</p>
|
||||
|
||||
<datalist id="users">
|
||||
@@ -327,3 +327,4 @@ document.addEventListener("DOMContentLoaded", ()=>{
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<vc:ui-extension-point location="prism-edit" model="@Model"></vc:ui-extension-point>
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**"/>
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj"/>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources"/>
|
||||
<Folder Include="Resources" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
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
|
||||
{
|
||||
@@ -15,11 +28,29 @@ namespace BTCPayServer.Plugins.SideShift
|
||||
{
|
||||
private readonly BTCPayServerClient _btcPayServerClient;
|
||||
private readonly SideShiftService _sideShiftService;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
|
||||
public SideShiftController(BTCPayServerClient btcPayServerClient, SideShiftService sideShiftService)
|
||||
public SideShiftController(BTCPayServerClient btcPayServerClient,
|
||||
SideShiftService sideShiftService,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings, ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
_sideShiftService = sideShiftService;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_storeRepository = storeRepository;
|
||||
_serializerSettings = serializerSettings;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@@ -78,5 +109,229 @@ namespace BTCPayServer.Plugins.SideShift
|
||||
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 += $"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}\">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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.SideShift
|
||||
{
|
||||
@@ -9,13 +16,16 @@ namespace BTCPayServer.Plugins.SideShift
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.4" }
|
||||
new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.4"}
|
||||
};
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<SideShiftService>();
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/SideShiftNav",
|
||||
"store-integrations-nav"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/PullPaymentViewInsert",
|
||||
"pullpayment-view"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/StoreIntegrationSideShiftOption",
|
||||
"store-integrations-list"));
|
||||
// Checkout v2
|
||||
@@ -34,7 +44,109 @@ namespace BTCPayServer.Plugins.SideShift
|
||||
"checkout-lightning-post-tabs"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/CheckoutEnd",
|
||||
"checkout-end"));
|
||||
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("SideShift/PrismEnhance",
|
||||
"prism-edit"));
|
||||
applicationBuilder.AddSingleton<IPluginHookFilter, PrismDestinationValidate>();
|
||||
applicationBuilder.AddSingleton<IPluginHookFilter, PrismClaimDestination>();
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PrismSideshiftDestination
|
||||
{
|
||||
public string ShiftCoin { get; set; }
|
||||
public string ShiftNetwork { get; set; }
|
||||
public string ShiftDestination { get; set; }
|
||||
public string ShiftMemo { get; set; }
|
||||
|
||||
public bool Valid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(ShiftCoin) && !string.IsNullOrEmpty(ShiftNetwork) &&
|
||||
!string.IsNullOrEmpty(ShiftDestination);
|
||||
}
|
||||
}
|
||||
|
||||
public class PrismDestinationValidate : IPluginHookFilter
|
||||
{
|
||||
public string Hook => "prism-destination-validate";
|
||||
public async Task<object> Execute(object args)
|
||||
{
|
||||
if (args is not string args1 || !args1.StartsWith("sideshift:")) return args;
|
||||
var json = JObject.Parse(args1.Substring("sideshift:".Length)).ToObject<PrismSideshiftDestination>();
|
||||
return json.Valid();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PrismClaimDestination : IPluginHookFilter
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
public string Hook => "prism-claim-destination";
|
||||
|
||||
public PrismClaimDestination(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
public async Task<object> Execute(object args)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
if (args is not string s || network is null)
|
||||
{
|
||||
return Task.FromResult(args);
|
||||
}
|
||||
if (args is not string args1 || !args1.StartsWith("sideshift:")) return args;
|
||||
var request = JObject.Parse(args1.Substring("sideshift:".Length)).ToObject<PrismSideshiftDestination>();
|
||||
if (!request.Valid())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var client = _httpClientFactory.CreateClient("sideshift");
|
||||
|
||||
|
||||
var shiftResponse = await client.PostAsJsonAsync("https://sideshift.ai/api/v2/shifts/variable", new
|
||||
{
|
||||
settleAddress = request.ShiftDestination,
|
||||
affiliateId = "qg0OrfHJV",
|
||||
settleMemo = request.ShiftMemo,
|
||||
depositCoin = "BTC",
|
||||
depositNetwork = "lightning",
|
||||
settleCoin = request.ShiftCoin,
|
||||
settleNetwork = request.ShiftNetwork,
|
||||
}
|
||||
);
|
||||
if (!shiftResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var shift = await shiftResponse.Content.ReadAsAsync<SideShiftController.ShiftResponse>();
|
||||
try
|
||||
{
|
||||
LNURL.LNURL.Parse(shift.depositAddress, out var lnurl);
|
||||
return new LNURLPayClaimDestinaton(shift.depositAddress);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (BOLT11PaymentRequest.TryParse(shift.depositAddress, out var bolt11, network.NBitcoinNetwork))
|
||||
{
|
||||
return new BoltInvoiceClaimDestination(shift.depositAddress, bolt11);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class SideShiftAvailableCoin
|
||||
{
|
||||
public string coin { get; set; }
|
||||
public string[] networks { get; set; }
|
||||
public string name { get; set; }
|
||||
public bool hasMemo { get; set; }
|
||||
public JToken fixedOnly { get; set; }
|
||||
public JToken variableOnly { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@using System.Net.Http
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Plugins.SideShift
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@{
|
||||
var client = HttpClientFactory.CreateClient("sideshift");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://sideshift.ai/api/v2/coins");
|
||||
var response = await client.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var coins = await response.Content.ReadAsStringAsync().ContinueWith(t => JsonConvert.DeserializeObject<List<SideShiftAvailableCoin>>(t.Result));
|
||||
var availableCoins = coins.SelectMany(coin => coin.networks.Select(s => (Coin: coin, Network: s)))
|
||||
.Where(tuple => (tuple.Coin.fixedOnly.Type == JTokenType.Boolean && !tuple.Coin.fixedOnly.Value<bool>()) || (
|
||||
tuple.Coin.fixedOnly is JArray varOnlyArray && varOnlyArray.All(v => v.Value<string>() != tuple.Network))).ToList();
|
||||
|
||||
}
|
||||
|
||||
<script>
|
||||
|
||||
const ssAvailableCoins = @Json.Serialize(availableCoins.ToDictionary(tuple=> $"{tuple.Coin.coin}_{tuple.Network}",tuple =>
|
||||
new {
|
||||
coin = tuple.Coin.name,
|
||||
code = tuple.Coin.coin,
|
||||
memo = tuple.Coin.hasMemo,
|
||||
network = tuple.Network
|
||||
}));
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const sideshiftDestinationButton = document.createElement("button");
|
||||
sideshiftDestinationButton.type= "button";
|
||||
sideshiftDestinationButton.className = "btn btn-primary btn-sm";
|
||||
sideshiftDestinationButton.innerText = "Generate SideShift destination";
|
||||
|
||||
document.getElementById("add-prism").insertAdjacentElement("afterend", sideshiftDestinationButton);
|
||||
|
||||
const modal = new bootstrap.Modal('#sideshiftModal');
|
||||
sideshiftDestinationButton.addEventListener("click", ev => modal.show());
|
||||
const selectedSideShiftCoin = document.getElementById("sscoin");
|
||||
const specifiedSideShiftDestination = document.getElementById("ssdest");
|
||||
const specifiedSideShiftMemo= document.getElementById("ssmemo");
|
||||
const shiftButton = document.getElementById("ssshift");
|
||||
let selectedCoin = null;
|
||||
const destinationContainer = document.getElementById("ss-dest-info");
|
||||
specifiedSideShiftDestination.addEventListener("input", ev1 => {
|
||||
|
||||
document.getElementById("ss-result").style.display = "none";
|
||||
if (isValid()){
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}
|
||||
});
|
||||
specifiedSideShiftMemo.addEventListener("input", ev1 => {
|
||||
if (isValid()){
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}else{
|
||||
shiftButton.setAttribute("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
isValid = ()=>{
|
||||
return selectedCoin && specifiedSideShiftDestination.value &&
|
||||
(!selectedCoin.memo || specifiedSideShiftMemo.value);
|
||||
};
|
||||
handleSelectChanges = ()=>{
|
||||
if (selectedSideShiftCoin.value){
|
||||
selectedCoin = ssAvailableCoins[selectedSideShiftCoin.value];
|
||||
destinationContainer.style.display = "block";
|
||||
if (selectedCoin){
|
||||
specifiedSideShiftMemo.parentElement.style.display = selectedCoin.memo ? "block" : "none";
|
||||
specifiedSideShiftMemo.value = selectedCoin.memo ? specifiedSideShiftMemo.value : "";
|
||||
}
|
||||
}else{
|
||||
destinationContainer.style.display = "none";
|
||||
}
|
||||
};
|
||||
selectedSideShiftCoin.addEventListener("change", ev1 => {
|
||||
|
||||
|
||||
handleSelectChanges();
|
||||
});
|
||||
shiftButton.addEventListener("click", ev1 => {
|
||||
|
||||
document.getElementById("ss-result-txt").value = "";
|
||||
if (isValid()){
|
||||
|
||||
shiftButton.setAttribute("disabled", "disabled");
|
||||
|
||||
|
||||
document.getElementById("ss-result").style.display = "block";
|
||||
document.getElementById("ss-result-txt").value = "sideshift:"+JSON.stringify({
|
||||
shiftCoin:selectedCoin.code,
|
||||
shiftNetwork: selectedCoin.network,
|
||||
shiftDestination: specifiedSideShiftDestination.value,
|
||||
shiftMemo: specifiedSideShiftMemo.value
|
||||
});
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
handleSelectChanges();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="modal" tabindex="-1" id="sideshiftModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Generate SideShift destination</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>This will generate a piece of code based on Sideshift configuration that can work as a valid destination in prism. Prism will then generate a "shift" on Sideshift and send the funds through LN to it, and Sideshift will send you the conversion. </p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Which coin should Sideshift send you</label>
|
||||
<select id="sscoin" class="form-select">
|
||||
@foreach (var opt in availableCoins)
|
||||
{
|
||||
<option value="@(opt.Coin.coin)_@(opt.Network)">@opt.Coin.name (@opt.Network)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div id="ss-dest-info" style="display: none">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Destination</label>
|
||||
<input type="text" id="ssdest" class="form-control"/>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Memo</label>
|
||||
<input type="text" id="ssmemo" class="form-control"/>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="ssshift" disabled="disabled">Generate code</button>
|
||||
|
||||
<div id="ss-result" class="form-group mt-4" style="display: none;">
|
||||
<label class="form-label">Generated code</label>
|
||||
<input type="text" id="ss-result-txt" class="form-control" readonly="readonly"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,186 @@
|
||||
@using System.Net.Http
|
||||
@using BTCPayServer.Plugins.SideShift
|
||||
@using Newtonsoft.Json
|
||||
@using Newtonsoft.Json.Linq
|
||||
@model BTCPayServer.Models.ViewPullPaymentModel
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@inject SideShiftService SideShiftService
|
||||
@{
|
||||
var ss = await SideShiftService.GetSideShiftForStore(Model.StoreId);
|
||||
if (ss?.Enabled is not true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var client = HttpClientFactory.CreateClient("sideshift");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://sideshift.ai/api/v2/coins");
|
||||
var response = await client.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var coins = await response.Content.ReadAsStringAsync().ContinueWith(t => JsonConvert.DeserializeObject<List<SideShiftAvailableCoin>>(t.Result));
|
||||
var availableCoins = coins.SelectMany(coin => coin.networks.Select(s => (Coin: coin, Network: s)))
|
||||
.Where(tuple => (tuple.Coin.fixedOnly.Type == JTokenType.Boolean && !tuple.Coin.fixedOnly.Value<bool>()) || (
|
||||
tuple.Coin.fixedOnly is JArray varOnlyArray && varOnlyArray.All(v => v.Value<string>() != tuple.Network))).ToList();
|
||||
|
||||
var potentialPaymentMethods = Model.PaymentMethods;//.Where(id => id.CryptoCode.Equals(Model.Currency, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (Model.IsPending && potentialPaymentMethods.Any() && availableCoins.Any())
|
||||
{
|
||||
<script>
|
||||
const url = @Json.Serialize(Url.Action("CreatePayout", "SideShift", new {pullPaymentId = Model.Id }))
|
||||
const ssAvailableCoins = @Json.Serialize(availableCoins.ToDictionary(tuple=> $"{tuple.Coin.coin}_{tuple.Network}",tuple =>
|
||||
new {
|
||||
coin = tuple.Coin.name,
|
||||
code = tuple.Coin.coin,
|
||||
memo = tuple.Coin.hasMemo,
|
||||
network = tuple.Network
|
||||
}));
|
||||
const ssPaymentMethods = @Json.Serialize(potentialPaymentMethods.Select(id => new { id = id.ToString(), name= id.ToPrettyString()}));
|
||||
document.addEventListener("DOMContentLoaded", ev => {
|
||||
const ssButton = document.createElement("button");
|
||||
ssButton.type= "button";
|
||||
ssButton.title = "Claim through SideShift";
|
||||
ssButton.classList.add("btn","btn-primary");
|
||||
ssButton.innerHTML=' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" alt="SideShift" class="icon"><g transform="translate(6,6)"><path d="M13.19 1.91A8 8 0 0 0 1.9 13.2L13.2 1.9Z" fill="currentColor"/><path d="M2.76 14.05a8 8 0 0 0 11.29-11.3l-11.3 11.3Z" fill="currentColor"/></g></svg>';
|
||||
const destElement = document.getElementById("Destination");
|
||||
destElement.parentElement.insertBefore(ssButton,destElement);
|
||||
const modal = new bootstrap.Modal('#sideshiftModal');
|
||||
ssButton.addEventListener("click", ev1 => modal.show());
|
||||
|
||||
const selectedSideShiftSource = document.getElementById("sspmi");
|
||||
const selectedSideShiftCoin = document.getElementById("sscoin");
|
||||
const specifiedSideShiftDestination = document.getElementById("ssdest");
|
||||
const specifiedSideShiftMemo= document.getElementById("ssmemo");
|
||||
const shiftButton = document.getElementById("ssshift");
|
||||
let selectedCoin = null;
|
||||
const destinationContainer = document.getElementById("ss-dest-info");
|
||||
specifiedSideShiftDestination.addEventListener("input", ev1 => {
|
||||
if (isValid()){
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}
|
||||
});
|
||||
specifiedSideShiftMemo.addEventListener("input", ev1 => {
|
||||
if (isValid()){
|
||||
shiftButton.removeAttribute("disabled");
|
||||
}else{
|
||||
shiftButton.setAttribute("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
isValid = ()=>{
|
||||
return selectedSideShiftSource.value && selectedCoin && specifiedSideShiftDestination.value &&
|
||||
(!selectedCoin.memo || specifiedSideShiftMemo.value);
|
||||
};
|
||||
handleSelectChanges = ()=>{
|
||||
if (selectedSideShiftSource.value && selectedSideShiftCoin.value){
|
||||
destinationContainer.style.display = "block";
|
||||
}else{
|
||||
destinationContainer.style.display = "none";
|
||||
}
|
||||
};
|
||||
selectedSideShiftSource.addEventListener("change", ev1 => {
|
||||
handleSelectChanges();
|
||||
});
|
||||
selectedSideShiftCoin.addEventListener("change", ev1 => {
|
||||
|
||||
handleSelectChanges();
|
||||
if (!selectedSideShiftCoin.value){
|
||||
return;
|
||||
}
|
||||
selectedCoin = ssAvailableCoins[selectedSideShiftCoin.value];
|
||||
if (selectedCoin){
|
||||
specifiedSideShiftMemo.parentElement.style.display = selectedCoin.memo ? "block" : "none";
|
||||
specifiedSideShiftMemo.value = selectedCoin.memo ? specifiedSideShiftMemo.value : "";
|
||||
}
|
||||
});
|
||||
shiftButton.addEventListener("click", ev1 => {
|
||||
if (isValid()){
|
||||
|
||||
document.getElementById("ss-server-errors").innerHTML = "";
|
||||
shiftButton.setAttribute("disabled", "disabled");
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: "@Model.Amount",
|
||||
paymentMethod: selectedSideShiftSource.value,
|
||||
shiftCurrency: selectedCoin.code,
|
||||
shiftNetwork: selectedCoin.network,
|
||||
destination: specifiedSideShiftDestination.value,
|
||||
memo: specifiedSideShiftMemo.value
|
||||
})
|
||||
}).then(async response => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const json = await response.json();
|
||||
let errorHtml = "";
|
||||
if(Array.isArray(json)){
|
||||
for (const jsonElement of json) {
|
||||
errorHtml += `${jsonElement.message}<br/>`;
|
||||
}
|
||||
}else if(json.message){
|
||||
errorHtml = json.message;
|
||||
}
|
||||
document.getElementById("ss-server-errors").innerHTML = errorHtml;
|
||||
}
|
||||
}).catch(err => {
|
||||
alert(err);
|
||||
}).finally(() => {
|
||||
shiftButton.removeAttribute("disabled");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
handleSelectChanges();
|
||||
});
|
||||
|
||||
</script>
|
||||
<div class="modal" tabindex="-1" id="sideshiftModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Claim through SideShift</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="ss-server-errors" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Which payment method should BTCPay Server send to Sideshift with</label>
|
||||
<select id="sspmi" class="form-select">
|
||||
@foreach (var opt in potentialPaymentMethods)
|
||||
{
|
||||
<option value="@opt.ToString()">@opt.ToPrettyString()</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Which coin should Sideshift send you</label>
|
||||
<select id="sscoin" class="form-select">
|
||||
@foreach (var opt in availableCoins)
|
||||
{
|
||||
<option value="@(opt.Coin.coin)_@(opt.Network)">@opt.Coin.name (@opt.Network)</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div id="ss-dest-info" style="display: none">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Destination</label>
|
||||
<input type="text" id="ssdest" class="form-control"/>
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Memo</label>
|
||||
<input type="text" id="ssmemo" class="form-control"/>
|
||||
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="ssshift" disabled="disabled">Claim payout through Sideshift</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Submodule submodules/btcpayserver updated: fcd50a3f6f...3fb5e85369
Reference in New Issue
Block a user