Compare commits

...

2 Commits

Author SHA1 Message Date
cf496f1d34 fixes 2020-01-16 05:59:48 +01:00
85d9d348f0 atomic swap 2020-01-15 13:14:51 +01:00
39 changed files with 1702 additions and 205 deletions

View File

@ -24,6 +24,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
BlockTime = TimeSpan.FromMinutes(10),
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()

View File

@ -26,7 +26,8 @@ namespace BTCPayServer
},
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
BlockTime = TimeSpan.FromMinutes(2.5)
});
}
}

View File

@ -32,6 +32,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
BlockTime = TimeSpan.FromMinutes(1),
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()

View File

@ -31,6 +31,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
BlockTime = TimeSpan.FromMinutes(1),
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()

View File

@ -131,6 +131,18 @@ namespace BTCPayServer
public string CryptoImagePath { get; set; }
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
public TimeSpan? BlockTime { get; internal set; }
public TimeSpan GetTimeSpan(int blockCount)
{
return new TimeSpan(BlockTime.Value.Ticks * blockCount);
}
public int GetBlockCount(TimeSpan span)
{
return (int)Math.Round(((double)span.Ticks / BlockTime.Value.Ticks), MidpointRounding.ToEven);
}
public override string ToString()
{
return CryptoCode;

View File

@ -2,10 +2,7 @@
using System.Linq;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -14,27 +11,18 @@ using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using OpenIddict.Abstractions;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Threading.Tasks;

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@ -10,10 +9,8 @@ using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;

View File

@ -1,26 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;

View File

@ -1,26 +1,18 @@
using System;
using BTCPayServer;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;
using System.IO;
using System.Net.Http;
using System.Reflection;
using BTCPayServer.Tests.Logging;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Views.Stores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.Interactions;
namespace BTCPayServer.Tests

View File

@ -1,28 +1,17 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using NBitcoin;
using NBitcoin.RPC;
using NBitpayClient;
using NBXplorer;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests

View File

@ -60,6 +60,7 @@ using Microsoft.Extensions.DependencyInjection;
using NBXplorer.DerivationStrategy;
using BTCPayServer.U2F.Models;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Views.Wallets;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
namespace BTCPayServer.Tests
@ -1488,6 +1489,104 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public async Task CanSwapCurrencies()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var userBTCWallet = user.RegisterDerivationScheme("BTC");
var userLTCWallet = user.RegisterDerivationScheme("LTC");
WalletId userBTCWallet2 = null;
WalletId userLTCWallet2 = null;
// For having faster test, let's create user2 concurrently
var creatingUser2 = Task.Run(() =>
{
var u = tester.NewAccount();
u.GrantAccess();
userBTCWallet2 = u.RegisterDerivationScheme("BTC");
userLTCWallet2 = u.RegisterDerivationScheme("LTC");
return u;
});
var atomics = user.GetController<WalletsController>();
var newXSwap = Assert.IsType<NewViewModel>(Assert.IsType<ViewResult>(atomics.NewAtomicSwap(userBTCWallet).Result).Model);
Assert.Equal(2, newXSwap.WalletList.Count());
Assert.Contains(userBTCWallet.ToString(), newXSwap.WalletList.Select(c => c.Value));
Assert.Contains(userLTCWallet.ToString(), newXSwap.WalletList.Select(c => c.Value));
newXSwap.Amount = 1.0;
newXSwap.Spread = 5;
newXSwap.RateRule = "coinaverage(BTC_USD) * coinaverage(USD_LTC);";
newXSwap.SelectedWallet = userLTCWallet.ToString();
Assert.IsType<RedirectToActionResult>(atomics.NewAtomicSwap(userBTCWallet, newXSwap).GetAwaiter().GetResult());
Assert.NotNull(atomics.CreatedOfferId);
var entry = atomics.AtomicSwapRepository.GetEntry(atomics.CreatedOfferId).Result;
Assert.NotNull(entry);
var offer = entry.Offer;
// The amount of BTC is less than the amount of LTC because it is more expensive
Assert.True(offer.Offer.Amount < offer.Price.Amount);
Assert.Equal("BTC", offer.Offer.CryptoCode);
Assert.Equal("LTC", offer.Price.CryptoCode);
Assert.Equal("coinaverage(BTC_USD) * coinaverage(USD_LTC)", offer.Rule);
// Let's check it the offer appear in the list
var list = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics.AtomicSwapList(userBTCWallet).Result).Model);
var item = list.Swaps[0];
Assert.Equal(XSwapRole.Maker.ToString(), item.Role);
Assert.Equal("1.00000000 BTC", item.Sent);
Assert.Equal("10.50000000 LTC", item.Received);
Assert.Equal(XSwapStatus.WaitingTaker.ToString(), item.Status);
// Let's get user2 take the offer
var user2 = creatingUser2.Result;
var apps2 = user2.GetController<AppsController>();
var atomics2 = user2.GetController<WalletsController>();
var takeVM = Assert.IsType<TakeViewModel>(Assert.IsType<ViewResult>(atomics2.TakeAtomicSwap(userBTCWallet2)).Model);
takeVM.MakerUri = atomics.AtomicSwapRepository.GetEntry(atomics.CreatedOfferId).Result.Offer.MarketMakerUri.ToString();
var list2 = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics2.AtomicSwapList(userBTCWallet2).Result).Model);
Assert.Empty(list2.Swaps);
atomics2.TakeAtomicSwap(userBTCWallet2, takeVM).Wait();
list = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics.AtomicSwapList(userBTCWallet).Result).Model);
list2 = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics2.AtomicSwapList(userBTCWallet2).Result).Model);
Assert.Single(list.Swaps);
Assert.Single(list2.Swaps);
Assert.Null(list.Swaps[0].Partner);
Assert.Equal("127.0.0.1", list2.Swaps[0].Partner);
Assert.Equal(XSwapStatus.WaitingTaker.ToString(), list.Swaps[0].Status);
Assert.Equal(XSwapStatus.WaitingTaker.ToString(), list2.Swaps[0].Status);
var takerVM = Assert.IsType<AtomicSwapDetailsTakerWaitingTakerViewModel>(Assert.IsType<ViewResult>(atomics2.AtomicSwapDetails(userBTCWallet2, atomics2.CreatedOfferId).Result).Model);
atomics2.AcceptAtomicSwapOffer(userBTCWallet2, atomics2.CreatedOfferId, takerVM).Wait();
list = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics.AtomicSwapList(userBTCWallet).Result).Model);
list2 = Assert.IsType<ListViewModel>(Assert.IsType<ViewResult>(atomics2.AtomicSwapList(userBTCWallet2).Result).Model);
Assert.Equal(XSwapStatus.WaitingEscrow.ToString(), list.Swaps[0].Status);
Assert.Equal(XSwapStatus.WaitingEscrow.ToString(), list2.Swaps[0].Status);
Assert.Equal(XSwapRole.Maker.ToString(), list.Swaps[0].Role);
Assert.Equal(XSwapRole.Taker.ToString(), list2.Swaps[0].Role);
Assert.Equal("1.00000000 BTC", list.Swaps[0].Sent);
Assert.Equal("10.50000000 LTC", list.Swaps[0].Received);
Assert.Equal("10.50000000 LTC", list2.Swaps[0].Sent);
Assert.Equal("1.00000000 BTC", list2.Swaps[0].Received);
var swapControler= Assert.IsType<AtomicSwapEscrowViewModel>(Assert.IsType<ViewResult>(atomics.AtomicSwapDetails(userBTCWallet, atomics.CreatedOfferId).Result).Model);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.AtomicSwaps
{
public class AtomicSwapClient
{
public AtomicSwapClient(Uri serverAddress)
{
if (serverAddress == null)
throw new ArgumentNullException(nameof(serverAddress));
ServerAddress = serverAddress;
}
private static readonly HttpClient SharedClient = new HttpClient();
internal HttpClient Client = SharedClient;
public void SetClient(HttpClient client)
{
Client = client;
}
public Uri ServerAddress { get; }
internal string GetFullUri(string relativePath, params object[] parameters)
{
relativePath = string.Format(CultureInfo.InvariantCulture, relativePath, parameters ?? Array.Empty<object>());
var uri = ServerAddress.AbsoluteUri;
if (!uri.EndsWith("/", StringComparison.Ordinal))
uri += "/";
uri += relativePath;
return uri;
}
private Task<T> GetAsync<T>(string relativePath, object[] parameters, CancellationToken cancellation)
{
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters, cancellation);
}
private async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, object[] parameters, CancellationToken cancellation)
{
HttpRequestMessage message = CreateMessage(method, body, relativePath, parameters);
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
if ((int)result.StatusCode == 404)
{
return default(T);
}
return await ParseResponse<T>(result).ConfigureAwait(false);
}
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, string relativePath, object[] parameters)
{
var uri = GetFullUri(relativePath, parameters);
var message = new HttpRequestMessage(method, uri);
if (body != null)
{
if (body is byte[])
message.Content = new ByteArrayContent((byte[])body);
else
message.Content = new StringContent(NBitcoin.JsonConverters.Serializer.ToString(body), Encoding.UTF8, "application/json");
}
return message;
}
private async Task<T> ParseResponse<T>(HttpResponseMessage response)
{
using (response)
{
if (response.IsSuccessStatusCode)
if (response.Content.Headers.ContentLength == 0)
return default(T);
else if (response.Content.Headers.ContentType.MediaType.Equals("application/json", StringComparison.Ordinal))
{
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return NBitcoin.JsonConverters.Serializer.ToObject<T>(str);
}
else if (response.Content.Headers.ContentType.MediaType.Equals("application/octet-stream", StringComparison.Ordinal))
{
return (T)(object)await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return default(T);
var aaa = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
//if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
response.EnsureSuccessStatusCode();
//var error = _Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
//if (error == null)
// response.EnsureSuccessStatusCode();
//throw error.AsException();
return default(T);
}
}
private Task ParseResponse(HttpResponseMessage response)
{
using (response)
{
if (response.IsSuccessStatusCode)
return Task.CompletedTask;
//if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
response.EnsureSuccessStatusCode();
//var error = _Serializer.ToObject<NBXplorerError>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
//if (error == null)
// response.EnsureSuccessStatusCode();
//throw error.AsException();
return Task.CompletedTask;
}
}
internal async Task<AtomicSwapOffer> GetOffer(CancellationToken cancellation = default)
{
return await SendAsync<AtomicSwapOffer>(HttpMethod.Get, null, "offer", null, cancellation);
}
public async Task<AtomicSwapTakeResponse> Take(AtomicSwapTakeRequest atomicSwapTakeRequest, CancellationToken cancellation = default)
{
return await SendAsync<AtomicSwapTakeResponse>(HttpMethod.Post, atomicSwapTakeRequest, "take", null, cancellation);
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.AtomicSwaps
{
[Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidateNeverAttribute]
public class AtomicSwapTakeRequest
{
[JsonConverter(typeof(KeyJsonConverter))]
public PubKey MakerSentCryptoPubkey { get; set; }
[JsonConverter(typeof(KeyJsonConverter))]
public PubKey MakerReceivedCryptoPubkey { get; set; }
public Uri TakerUri { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.AtomicSwaps
{
public class AtomicSwapTakeResponse
{
[JsonConverter(typeof(UInt160JsonConverter))]
public uint160 Hash { get; set; }
[JsonConverter(typeof(KeyJsonConverter))]
public PubKey MakerSentCryptoPubkey { get; set; }
[JsonConverter(typeof(KeyJsonConverter))]
public PubKey MakerReceivedCryptoPubkey { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.Crypto;
namespace BTCPayServer.AtomicSwaps
{
public class Preimage
{
public Preimage()
{
Bytes = RandomUtils.GetBytes(32);
}
public Preimage(byte[] bytes)
{
Bytes = bytes;
}
public byte[] Bytes
{
get; set;
}
public uint160 GetHash()
{
return new uint160(Hashes.Hash160(Bytes, Bytes.Length));
}
}
}

View File

@ -1,135 +1,135 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')"/>
<Import Project="../Build/Common.csproj"/>
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
<Compile Remove="Build\**"/>
<Compile Remove="wwwroot\bundles\jqueryvalidate\**"/>
<Compile Remove="wwwroot\vendor\jquery-nice-select\**"/>
<Content Remove="Build\**"/>
<Content Remove="wwwroot\bundles\jqueryvalidate\**"/>
<Content Remove="wwwroot\vendor\jquery-nice-select\**"/>
<EmbeddedResource Remove="Build\**"/>
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**"/>
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**"/>
<None Remove="Build\**"/>
<None Remove="wwwroot\bundles\jqueryvalidate\**"/>
<None Remove="wwwroot\vendor\jquery-nice-select\**"/>
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
<None Remove="Currencies.txt"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
<EmbeddedResource Include="bundleconfig.json"/>
<EmbeddedResource Include="Currencies.txt"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.8" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
<PackageReference Include="LedgerWallet" Version="2.0.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3"/>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.8"/>
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435"/>
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435"/>
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435"/>
<PackageReference Include="HtmlSanitizer" Version="4.0.217"/>
<PackageReference Include="LedgerWallet" Version="2.0.0.5"/>
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2"/>
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitpayClient" Version="1.0.0.35" />
<PackageReference Include="DBriize" Version="1.0.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="NBitpayClient" Version="1.0.0.35"/>
<PackageReference Include="DBriize" Version="1.0.1.3"/>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3"/>
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2"/>
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3"/>
<PackageReference Include="NicolasDorier.RateLimits" Version="1.1.0"/>
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18"/>
<PackageReference Include="Serilog" Version="2.9.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0"/>
<PackageReference Include="SSH.NET" Version="2016.1.0"/>
<PackageReference Include="Text.Analyzers" Version="2.6.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="TwentyTwenty.Storage" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
<ItemGroup>
<PackageReference Include="TwentyTwenty.Storage" Version="2.12.1"/>
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.12.1"/>
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1"/>
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1"/>
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1"/>
<PackageReference Include="U2F.Core" Version="1.0.4"/>
<PackageReference Include="YamlDotNet" Version="8.0.0"/>
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15"/>
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" Condition="'$(Configuration)' == 'Debug'"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0"/>
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\main\bootstrap4-creativestart\creative.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less" />
<None Include="wwwroot\vendor\font-awesome\less\core.less" />
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less" />
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less" />
<None Include="wwwroot\vendor\font-awesome\less\icons.less" />
<None Include="wwwroot\vendor\font-awesome\less\larger.less" />
<None Include="wwwroot\vendor\font-awesome\less\list.less" />
<None Include="wwwroot\vendor\font-awesome\less\mixins.less" />
<None Include="wwwroot\vendor\font-awesome\less\path.less" />
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less" />
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less" />
<None Include="wwwroot\vendor\font-awesome\less\stacked.less" />
<None Include="wwwroot\vendor\font-awesome\less\variables.less" />
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
<None Include="wwwroot\vendor\jquery\jquery.js" />
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.min.js" />
<None Include="wwwroot\vendor\popper\popper.js" />
<None Include="wwwroot\vendor\popper\popper.min.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.min.js" />
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\main\bootstrap4-creativestart\creative.js"/>
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg"/>
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2"/>
<None Include="wwwroot\vendor\font-awesome\less\animated.less"/>
<None Include="wwwroot\vendor\font-awesome\less\bordered-pulled.less"/>
<None Include="wwwroot\vendor\font-awesome\less\core.less"/>
<None Include="wwwroot\vendor\font-awesome\less\fixed-width.less"/>
<None Include="wwwroot\vendor\font-awesome\less\font-awesome.less"/>
<None Include="wwwroot\vendor\font-awesome\less\icons.less"/>
<None Include="wwwroot\vendor\font-awesome\less\larger.less"/>
<None Include="wwwroot\vendor\font-awesome\less\list.less"/>
<None Include="wwwroot\vendor\font-awesome\less\mixins.less"/>
<None Include="wwwroot\vendor\font-awesome\less\path.less"/>
<None Include="wwwroot\vendor\font-awesome\less\rotated-flipped.less"/>
<None Include="wwwroot\vendor\font-awesome\less\screen-reader.less"/>
<None Include="wwwroot\vendor\font-awesome\less\stacked.less"/>
<None Include="wwwroot\vendor\font-awesome\less\variables.less"/>
<None Include="wwwroot\vendor\font-awesome\scss\font-awesome.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_animated.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_bordered-pulled.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_core.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_fixed-width.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_icons.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_larger.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_list.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_mixins.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_path.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_rotated-flipped.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss"/>
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss"/>
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js"/>
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js"/>
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js"/>
<None Include="wwwroot\vendor\jquery\jquery.js"/>
<None Include="wwwroot\vendor\jquery\jquery.min.js"/>
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.js"/>
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.min.js"/>
<None Include="wwwroot\vendor\popper\popper.js"/>
<None Include="wwwroot\vendor\popper\popper.min.js"/>
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.js"/>
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.min.js"/>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\vendor\clipboard.js\"/>
<Folder Include="wwwroot\vendor\highlightjs\"/>
<Folder Include="wwwroot\vendor\summernote"/>
<Folder Include="wwwroot\vendor\u2f"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj"/>
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj"/>
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj"/>
</ItemGroup>
<ItemGroup>
<ItemGroup>
<Content Update="Views\Apps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
@ -158,6 +158,27 @@
<Content Update="Views\Server\DynamicDnsService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\AtomicSwapDetailsMarkerWaitingTaker.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\AtomicSwapEscrow.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\AtomicSwapDetailsTakerWaitingTaker.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\AtomicSwapDetails.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\TakeAtomicSwap.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\NewAtomicSwap.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\AtomicSwapList.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
@ -167,55 +188,55 @@
<Content Update="Views\Stores\PayButtonEnable.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBT.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendVault.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LndServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Maintenance.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletPSBT.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletRescan.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendVault.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Remove="Views\Server\EditGoogleCloudStorageStorageProvider.cshtml">
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewImports.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewStart.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewImports.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewStart.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.AtomicSwaps;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using NBitcoin;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
[Route("api/xswap")]
public partial class AtomicSwapController : Controller
{
public AtomicSwapController(AtomicSwapRepository atomicSwapRepository, AtomicSwapClientFactory atomicSwapClientFactory)
{
AtomicSwapRepository = atomicSwapRepository;
AtomicSwapClientFactory = atomicSwapClientFactory;
}
public AtomicSwapRepository AtomicSwapRepository { get; }
public AtomicSwapClientFactory AtomicSwapClientFactory { get; }
[Route("{offerId}/offer")]
public async Task<IActionResult> GetOfferAPI(string offerId)
{
var entry = await AtomicSwapRepository.GetEntry(offerId);
if (entry == null ||
(entry.Status != XSwapStatus.WaitingTaker && entry.Role == XSwapRole.Maker) ||
(entry.Status != XSwapStatus.WaitingEscrow && entry.Role == XSwapRole.Taker))
return NotFound();
return Json(entry.Offer);
}
[Route("{offerId}/take")]
[HttpPost]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> TakeOfferAPI(string offerId, [FromBody] AtomicSwapTakeRequest request)
{
var entry = await AtomicSwapRepository.GetEntry(offerId);
if (entry == null || entry.Status != XSwapStatus.WaitingTaker)
return NotFound();
// TODO atomically take the offer
var client = AtomicSwapClientFactory.Create(request.TakerUri);
AtomicSwapTakeResponse response = null;
try
{
using (var cts = new CancellationTokenSource())
{
cts.CancelAfter(5000);
var takerOffer = await client.GetOffer(cts.Token);
if (takerOffer.MarketMakerUri != entry.Offer.MarketMakerUri)
return NotFound();
}
}
catch { }
entry.Partner = request.TakerUri.DnsSafeHost;
entry.OtherUri = request.TakerUri;
entry.Status = XSwapStatus.WaitingEscrow;
entry.Sent.MyKey = new Key();
entry.Sent.OtherKey = request.MakerSentCryptoPubkey;
entry.Received.MyKey = new Key();
entry.Received.OtherKey = request.MakerReceivedCryptoPubkey;
entry.Preimage = new Preimage();
entry.Hash = entry.Preimage.GetHash();
response = new AtomicSwapTakeResponse()
{
Hash = entry.Preimage.GetHash(),
MakerReceivedCryptoPubkey = entry.Received.MyKey.PubKey,
MakerSentCryptoPubkey = entry.Received.MyKey.PubKey
};
await AtomicSwapRepository.UpdateEntry(offerId, entry);
return Json(response);
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
@ -17,6 +18,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Views.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -40,8 +42,10 @@ namespace BTCPayServer.Controllers
{
public StoreRepository Repository { get; }
public WalletRepository WalletRepository { get; }
public AtomicSwapRepository AtomicSwapRepository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
public AtomicSwapClientFactory AtomicSwapClientFactory { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly JsonSerializerSettings _serializerSettings;
@ -53,6 +57,7 @@ namespace BTCPayServer.Controllers
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
AtomicSwapRepository atomicSwapRepository,
WalletRepository walletRepository,
CurrencyNameTable currencyTable,
BTCPayNetworkProvider networkProvider,
@ -63,7 +68,8 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
BTCPayWalletProvider walletProvider,
AtomicSwapClientFactory atomicSwapClientFactory)
{
_currencyTable = currencyTable;
Repository = repo;
@ -77,6 +83,8 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
AtomicSwapClientFactory = atomicSwapClientFactory;
AtomicSwapRepository = atomicSwapRepository;
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md

View File

@ -0,0 +1,398 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.AtomicSwaps;
using BTCPayServer.Data;
using BTCPayServer.ModelBinders;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{
public partial class WalletsController : Controller
{
[Route("{walletId}/xswap")]
public async Task<IActionResult> AtomicSwapList([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
{
var derivationStrategy = await GetDerivationStrategy(walletId);
if (derivationStrategy == null)
return NotFound();
ListViewModel list = new ListViewModel();
list.CryptoCode = walletId.CryptoCode;
foreach (var entry in AtomicSwapRepository.GetEntries(walletId))
{
ListViewModel.SwapItem item = new ListViewModel.SwapItem()
{
WalletId = walletId.ToString(),
OfferId = entry.Id,
Partner = entry.Partner,
Role = entry.Role.ToString(),
Status = entry.Status.ToString(),
Timestamp = entry.Offer.CreatedAt,
Sent = FormatAmount(entry.Sent),
Received = FormatAmount(entry.Received),
};
list.Swaps.Add(item);
}
return View(list);
}
private string FormatAmount(AtomicSwapEscrowData entry)
{
return _currencyTable.DisplayFormatCurrency(entry.Amount.ToDecimal(MoneyUnit.BTC), entry.CryptoCode);
}
[Route("{walletId}/xswap/new")]
public async Task<IActionResult> NewAtomicSwap([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
{
var derivationStrategy = await GetDerivationStrategy(walletId);
if (derivationStrategy == null)
return NotFound();
var storeData = await Repository.FindStore(walletId.StoreId, GetUserId());
var wallets = await GetNamedWallets(walletId.CryptoCode);
var newVM = new NewViewModel();
var rateRules = storeData.GetStoreBlob().GetRateRules(NetworkProvider);
newVM.SetWalletList(wallets, walletId.ToString());
newVM.CryptoCode = walletId.CryptoCode;
return View(newVM);
}
private async Task<NamedWallet[]> GetNamedWallets(string fromCrypto)
{
var stores = await Repository.GetStoresByUserId(GetUserId());
return stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Where(p => p.Network.BlockTime != null)
.Where(p => p != null && ExplorerClientProvider.IsAvailable(p.Network))
.Select(p => new NamedWallet()
{
Name = $"{p.PaymentId.CryptoCode}: {s.StoreName}",
DerivationStrategy = p,
CryptoCode = p.PaymentId.CryptoCode,
WalletId = new WalletId(s.Id, p.PaymentId.CryptoCode),
Rule = GetRuleNoSpread(s.GetStoreBlob(), fromCrypto, p.PaymentId.CryptoCode),
Spread = s.GetStoreBlob().Spread
}))
.ToArray();
}
private RateRule GetRuleNoSpread(StoreBlob storeBlob, string cryptoCodeA, string cryptoCodeB)
{
var rules = storeBlob.GetRateRules(NetworkProvider);
rules.Spread = 0;
var rule = rules.GetRuleFor(new CurrencyPair(cryptoCodeA, cryptoCodeB));
return rule;
}
[TempData]
public string StatusMessage { get; set; }
public string CreatedOfferId { get; private set; }
[Route("{walletId}/xswap/new")]
[HttpPost]
public async Task<IActionResult> NewAtomicSwap(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
NewViewModel newVM)
{
var fromWallet = await GetDerivationStrategy(walletId);
var statusAsync = ExplorerClientProvider.GetExplorerClient(fromWallet.Network).GetStatusAsync();
if (fromWallet == null)
return NotFound();
var wallets = await GetNamedWallets(walletId.CryptoCode);
newVM.SetWalletList(wallets, newVM.SelectedWallet);
newVM.CryptoCode = fromWallet.Network.CryptoCode;
if (!WalletId.TryParse(newVM.SelectedWallet, out var selectedWalletId))
{
ModelState.AddModelError(nameof(newVM.SelectedWallet), "Invalid wallet id");
return View(newVM);
}
var toWallet = await GetDerivationStrategy(selectedWalletId);
if (toWallet == null)
{
ModelState.AddModelError(nameof(newVM.SelectedWallet), "Invalid wallet id");
return View(newVM);
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
AtomicSwapOffer offer = new AtomicSwapOffer();
offer.MarketMakerUri = new Uri($"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}api/xswap/{id}", UriKind.Absolute);
offer.Offer = new AtomicSwapOfferAsset()
{
Amount = Money.Coins((decimal)newVM.Amount),
CryptoCode = walletId.CryptoCode,
};
var minRelayFee = (await statusAsync).BitcoinStatus.MinRelayTxFee;
var minimumAmount = minRelayFee.GetFee(200); // Arbitrary but should cover the dust of any output
if (offer.Offer.Amount <= minimumAmount)
{
ModelState.AddModelError(nameof(newVM.Amount), $"Amount must be above {minimumAmount}");
return View(newVM);
}
offer.Price = new AtomicSwapOfferAsset()
{
CryptoCode = toWallet.PaymentId.CryptoCode
};
var lockTimespan = TimeSpan.FromDays(2);
offer.CreatedAt = DateTimeOffset.UtcNow;
var storeData = await Repository.FindStore(walletId.StoreId, GetUserId());
if (ModelState.IsValid)
{
var pair = new CurrencyPair("AAA", "BBB");
newVM.RateRule = $"{pair} = {newVM.RateRule}";
if (RateRules.TryParse(newVM.RateRule, out var rules, out var rateRulesErrors))
{
rules.Spread = (decimal)newVM.Spread / 100.0m;
var rateResult = await RateFetcher.FetchRate(pair, rules, CancellationToken.None);
if (rateResult.BidAsk == null)
{
string errorMessage = "Error when fetching rate";
if (rateResult.EvaluatedRule != null)
{
errorMessage += $" ({rateResult.EvaluatedRule})";
}
ModelState.AddModelError(nameof(newVM.RateRule), errorMessage);
}
else
{
offer.Price.Amount = Money.Coins(offer.Offer.Amount.ToDecimal(MoneyUnit.BTC) * rateResult.BidAsk.Ask);
rules.Spread = 0;
offer.Rule = rules.GetRuleFor(pair).ToString();
}
}
else
{
string errorDetails = "";
if (rateRulesErrors.Count > 0)
{
errorDetails = $" ({rateRulesErrors[0]})";
}
ModelState.AddModelError(nameof(newVM.RateRule), $"Impossible to parse rate rules{errorDetails}");
}
}
if (!ModelState.IsValid)
return View(newVM);
var statusSent = ExplorerClientProvider.GetExplorerClient(offer.Offer.CryptoCode).GetStatusAsync();
var statusReceived = ExplorerClientProvider.GetExplorerClient(offer.Price.CryptoCode).GetStatusAsync();
offer.Offer.LockTime = new LockTime((await statusSent).ChainHeight + fromWallet.Network.GetBlockCount(lockTimespan));
offer.Price.LockTime = new LockTime((await statusReceived).ChainHeight + toWallet.Network.GetBlockCount(lockTimespan));
StatusMessage = $"Offer created, share the following link with the marker takers: {offer.MarketMakerUri}";
CreatedOfferId = id;
var entry = new AtomicSwapEntry();
entry.Offer = offer;
entry.Role = XSwapRole.Maker;
entry.Status = XSwapStatus.WaitingTaker;
entry.Sent = new SentAtomicSwapAsset(offer.Offer)
{
Refund = await GetDestination(fromWallet),
WalletId = walletId,
};
entry.Received = new ReceivedAtomicSwapAsset(offer.Price)
{
Destination = await GetDestination(toWallet),
WalletId = selectedWalletId
};
await AtomicSwapRepository.SaveEntry(walletId, id, entry);
return RedirectToAction(nameof(AtomicSwapDetails), new { offerId = id, walletId = walletId.ToString() });
}
private async Task<Script> GetDestination(DerivationSchemeSettings derivationStrategy)
{
var explorer = this.ExplorerClientProvider.GetExplorerClient(derivationStrategy.PaymentId.CryptoCode);
var scriptPubKey = (await explorer.GetUnusedAsync(derivationStrategy.AccountDerivation, NBXplorer.DerivationStrategy.DerivationFeature.Deposit, reserve: true)).ScriptPubKey;
return scriptPubKey;
}
[Route("{walletId}/xswap/take")]
public IActionResult TakeAtomicSwap(WalletId walletId)
{
return View(new TakeViewModel());
}
[Route("{walletId}/xswap/{offerId}/details")]
public async Task<IActionResult> AtomicSwapDetails(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string offerId)
{
var derivationStrategy = await GetDerivationStrategy(walletId);
if (derivationStrategy == null)
return NotFound();
var offer = await AtomicSwapRepository.GetEntry(offerId);
if (offer.Status == XSwapStatus.WaitingTaker)
{
if (offer.Role == XSwapRole.Maker)
{
var vm = new AtomicSwapDetailsMarkerWaitingTakerViewModel()
{
ToSend = FormatAmount(offer.Sent),
ToReceive = FormatAmount(offer.Received),
OfferUri = new Uri($"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}api/xswap/{offerId}", UriKind.Absolute)
};
return View("AtomicSwapDetailsMarkerWaitingTaker", vm);
}
if (offer.Role == XSwapRole.Taker)
{
var vm = new AtomicSwapDetailsTakerWaitingTakerViewModel()
{
ToSend = FormatAmount(offer.Sent),
ToReceive = FormatAmount(offer.Received),
WalletId = walletId.ToString(),
RefundTime = await GetRefundTime(offer),
};
var wallets = (await GetNamedWallets(offer.Received.CryptoCode)).Where(w => w.DerivationStrategy.PaymentId.CryptoCode == offer.Received.CryptoCode).ToArray();
vm.SetWalletList(wallets, null);
return View("AtomicSwapDetailsTakerWaitingTaker", vm);
}
}
else if (offer.Status == XSwapStatus.WaitingEscrow)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(offer.Sent.CryptoCode);
var vm = new AtomicSwapEscrowViewModel()
{
ToSend = FormatAmount(offer.Sent),
ToReceive = FormatAmount(offer.Received),
SentToWalletId = walletId.ToString(),
SentFromWalletId = offer.Sent.WalletId.ToString(),
EscrowAddress = offer.Sent.GetEscrow().ToScript().GetDestinationAddress(network.NBitcoinNetwork).ToString(),
Amount = offer.Sent.Amount.ToString(),
RefundTime = await GetRefundTime(offer)
};
return View("AtomicSwapEscrow", vm);
}
return NotFound();
}
public class RefundTime
{
public int BlockCount { get; set; }
public TimeSpan Time { get; set; }
}
private async Task<RefundTime> GetRefundTime(AtomicSwapEntry offer)
{
var status = await ExplorerClientProvider.GetExplorerClient(offer.Sent.CryptoCode).GetStatusAsync();
var network = NetworkProvider.GetNetwork(offer.Sent.CryptoCode);
var blocksToWait = Math.Max(0, offer.Sent.LockTime.Height - status.ChainHeight);
var refundTime = network.GetTimeSpan(blocksToWait);
return new RefundTime() { BlockCount = blocksToWait, Time = refundTime };
}
[Route("{walletId}/xswap/take")]
[HttpPost]
public async Task<IActionResult> TakeAtomicSwap([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, TakeViewModel takeViewModel)
{
var derivationStrategy = await GetDerivationStrategy(walletId);
var makerUri = new Uri(takeViewModel.MakerUri, UriKind.Absolute);
AtomicSwapClient client = AtomicSwapClientFactory.Create(makerUri);
var offer = await client.GetOffer();
if (offer == null)
{
ModelState.AddModelError(nameof(takeViewModel.MakerUri), "Offer not found");
}
else
{
if (offer.Offer.CryptoCode != walletId.CryptoCode)
{
ModelState.AddModelError(nameof(takeViewModel.MakerUri), $"Offer for {offer.Offer.CryptoCode}, but this wallet is for {walletId.CryptoCode}");
}
}
if (!ModelState.IsValid)
return View(takeViewModel);
var entry = new AtomicSwapEntry()
{
Partner = new Uri(takeViewModel.MakerUri, UriKind.Absolute).DnsSafeHost,
OtherUri = makerUri,
Offer = offer,
Received = new ReceivedAtomicSwapAsset(offer.Offer)
{
OtherKey = offer.Offer.Pubkey,
Destination = await GetDestination(derivationStrategy),
WalletId = walletId
},
Sent = new SentAtomicSwapAsset(offer.Price)
{
OtherKey = offer.Price.Pubkey,
},
Role = XSwapRole.Taker,
Status = XSwapStatus.WaitingTaker
};
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
CreatedOfferId = id;
await AtomicSwapRepository.SaveEntry(walletId, id, entry);
return RedirectToAction(nameof(AtomicSwapDetails), new { offerId = id, walletId = walletId.ToString() });
}
[Route("{walletId}/xswap/{offerId}/accept")]
public async Task<IActionResult> AcceptAtomicSwapOffer(
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
string offerId, AtomicSwapDetailsTakerWaitingTakerViewModel viewModel)
{
var derivationStrategy = await GetDerivationStrategy(walletId);
if (derivationStrategy == null)
return NotFound();
var entry = await AtomicSwapRepository.GetEntry(offerId);
if (entry == null)
return NotFound();
WalletId.TryParse(viewModel.SelectedWallet, out var selectedWalletId);
var destinationStrategy = await GetDerivationStrategy(selectedWalletId);
entry.Sent.WalletId = walletId;
entry.Sent.Refund = await GetDestination(derivationStrategy);
entry.Sent.MyKey = new Key();
entry.Received.Destination = await GetDestination(destinationStrategy);
entry.Received.WalletId = selectedWalletId;
entry.Received.MyKey = new Key();
entry.Status = XSwapStatus.WaitingEscrow;
var maker = AtomicSwapClientFactory.Create(entry.OtherUri);
var response = await maker.Take(new AtomicSwapTakeRequest()
{
TakerUri = new Uri($"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}api/xswap/{entry.Id}", UriKind.Absolute),
MakerReceivedCryptoPubkey = entry.Sent.MyKey.PubKey,
MakerSentCryptoPubkey = entry.Received.MyKey.PubKey,
});
if (response == null)
return NotFound();
entry.Received.OtherKey = response.MakerSentCryptoPubkey;
entry.Sent.OtherKey = response.MakerReceivedCryptoPubkey;
entry.Hash = response.Hash;
await AtomicSwapRepository.UpdateEntry(offerId, entry);
return RedirectToAction(nameof(AtomicSwapDetails), new { offerId = offerId, walletId = walletId.ToString() });
}
async Task<DerivationSchemeSettings> GetDerivationStrategy(WalletId walletId)
{
if (walletId?.StoreId == null || GetUserId() == null)
return null;
var storeData = await this.Repository.FindStore(walletId.StoreId, GetUserId());
var strategy = storeData.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(s => s.PaymentId.CryptoCode == walletId.CryptoCode);
if (strategy == null || !ExplorerClientProvider.IsAvailable(strategy.Network))
return null;
return strategy;
}
}
}

View File

@ -0,0 +1,114 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
//<Bob.PubKey>
//OP_DEPTH OP_3 OP_EQUAL
//OP_IF
// OP_SWAP
// <Alice.PubKey> OP_CHECKSIGVERIFY OP_CODESEPARATOR
//OP_ELSE
// 0 OP_CLTV OP_DROP
//OP_ENDIF
//OP_CHECKSIG
public class EscrowScriptPubKeyParameters
{
public EscrowScriptPubKeyParameters()
{
}
public EscrowScriptPubKeyParameters(PubKey initiator, PubKey receiver, LockTime lockTime)
{
this.Initiator = initiator;
this.Receiver = receiver;
this.LockTime = lockTime;
}
public PubKey Initiator
{
get; set;
}
public PubKey Receiver
{
get; set;
}
public LockTime LockTime
{
get; set;
}
static readonly Comparer<PubKey> LexicographicComparer = Comparer<PubKey>.Create((a, b) => Comparer<string>.Default.Compare(a?.ToHex(), b?.ToHex()));
// OP_DEPTH 2 OP_EQUAL
// OP_IF
// <Receiver.PubKey> OP_CHECKSIGVERIFY
// OP_ELSE
// 0 OP_CLTV OP_DROP
// OP_ENDIF
// <Initiator.PubKey> OP_CHECKSIG
public Script ToRedeemScript()
{
if (Initiator == null || Receiver == null || LockTime == default(LockTime))
throw new InvalidOperationException("Parameters are incomplete");
EscrowScriptPubKeyParameters parameters = new EscrowScriptPubKeyParameters();
List<Op> ops = new List<Op>();
ops.Add(OpcodeType.OP_DEPTH);
ops.Add(OpcodeType.OP_2);
ops.Add(OpcodeType.OP_EQUAL);
ops.Add(OpcodeType.OP_IF);
{
ops.Add(Op.GetPushOp(Receiver.ToBytes()));
ops.Add(OpcodeType.OP_CHECKSIGVERIFY);
}
ops.Add(OpcodeType.OP_ELSE);
{
ops.Add(Op.GetPushOp(LockTime));
ops.Add(OpcodeType.OP_CHECKLOCKTIMEVERIFY);
ops.Add(OpcodeType.OP_DROP);
}
ops.Add(OpcodeType.OP_ENDIF);
ops.Add(Op.GetPushOp(Initiator.ToBytes()));
ops.Add(OpcodeType.OP_CHECKSIG);
return new Script(ops.ToArray());
}
public override bool Equals(object obj)
{
EscrowScriptPubKeyParameters item = obj as EscrowScriptPubKeyParameters;
if (item == null)
return false;
return ToRedeemScript().Equals(item.ToRedeemScript());
}
public static bool operator ==(EscrowScriptPubKeyParameters a, EscrowScriptPubKeyParameters b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToRedeemScript() == b.ToRedeemScript();
}
public static bool operator !=(EscrowScriptPubKeyParameters a, EscrowScriptPubKeyParameters b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToRedeemScript().GetHashCode();
}
internal Script ToScript()
{
return ToRedeemScript().WitHash.ScriptPubKey.Hash.ScriptPubKey;
}
}
}

View File

@ -53,6 +53,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Views.Wallets;
using Serilog;
namespace BTCPayServer.Hosting
@ -69,6 +70,8 @@ namespace BTCPayServer.Hosting
o.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
});
services.AddHttpClient();
services.TryAddSingleton<AtomicSwapRepository>();
services.TryAddSingleton<AtomicSwapClientFactory>();
services.AddHttpClient(nameof(ExplorerClientProvider), httpClient =>
{
httpClient.Timeout = Timeout.InfiniteTimeSpan;

View File

@ -0,0 +1,23 @@
using System;
using System.Net.Http;
using BTCPayServer.AtomicSwaps;
namespace BTCPayServer.Services
{
public class AtomicSwapClientFactory
{
public AtomicSwapClientFactory(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
}
public IHttpClientFactory HttpClientFactory { get; }
public AtomicSwapClient Create(Uri serverUri)
{
var client = new AtomicSwapClient(serverUri);
client.SetClient(HttpClientFactory.CreateClient());
return client;
}
}
}

View File

@ -0,0 +1,21 @@
@*@model OfferViewModel
@{
Layout = "_Layout.cshtml";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@Model.Title</h2>
<hr class="primary">
<p>You need to send <b>@Model.ToSend</b> to <b>@Model.EscrowAddress</b>, if the other party get unresponsive, you will get back money on your <a href="@Model.WalletUrl">BTCPay store's wallet</a> after @Model.RefundTime.</p>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
</div>
</div>
</div>
</section>*@

View File

@ -0,0 +1,21 @@
@model AtomicSwapDetailsMarkerWaitingTakerViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage atomic swaps";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<div class="row">
<div class="col-md-10">
<p>You will buy <b>@Model.ToReceive</b> against <b>@Model.ToSend</b>, you need to find a partner for this trade.</p>
</div>
</div>
<div class="row">
<div class="col-md-10 text-center">
<div class="alert alert-warning alert-dismissible" role="alert">
<span>Share the following link with the party willing to take your offer</span>
<span><b>@Model.OfferUri</b></span>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public class AtomicSwapDetailsMarkerWaitingTakerViewModel
{
public string ToSend { get; set; }
public string ToReceive { get; internal set; }
public Uri OfferUri { get; set; }
}
}

View File

@ -0,0 +1,27 @@
@model AtomicSwapDetailsTakerWaitingTakerViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage atomic swaps";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<div class="row">
<div class="col-md-10">
<p>You will buy <b>@Model.ToReceive</b> against <b>@Model.ToSend</b>, if your peer get unresponsive, you will get back your money on <a asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">your wallet</a> in <b>@Model.RefundTime.Time.TimeString() (@Model.RefundTime.BlockCount blocks remaining)</b>.</p>
</div>
</div>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<label asp-for="SelectedWallet" class="control-label"></label>
<select asp-for="SelectedWallet" asp-items="Model.WalletList" class="form-control"></select>
<span asp-validation-for="SelectedWallet" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Accept offer" asp-action="AcceptAtomicSwapOffer" class="btn btn-primary" />
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Views.Wallets
{
public class AtomicSwapDetailsTakerWaitingTakerViewModel
{
public string ToSend { get; set; }
public string ToReceive { get; internal set; }
public string WalletId { get; set; }
public Controllers.WalletsController.RefundTime RefundTime { get; set; }
[Display(Name = "Receive on wallet...")]
public string SelectedWallet { get; set; }
public SelectList WalletList { get; set; }
public void SetWalletList(NamedWallet[] namedWallet, string selectedWallet)
{
var choices = namedWallet.Select(o => new { Name = o.Name, Value = o.WalletId.ToString() }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == selectedWallet) ?? choices.FirstOrDefault();
WalletList = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedWallet = chosen.Value;
}
}
}

View File

@ -0,0 +1,25 @@
@model AtomicSwapEscrowViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage atomic swaps";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<div class="row">
<div class="col-md-10">
<p>You will buy <b>@Model.ToReceive</b> against <b>@Model.ToSend</b>, if your peer get unresponsive, you will get back your money in <a asp-action="WalletTransactions" asp-route-walletId="@Model.SentToWalletId">your wallet</a> in <b>@Model.RefundTime.Time.TimeString() (@Model.RefundTime.BlockCount blocks remaining)</b>.</p>
</div>
</div>
<div class="row">
<div class="col-md-10">
<form asp-action="WalletSend" method="get">
<div class="form-group">
<input type="submit" value="Proceed to payment" class="btn btn-primary" />
<input type="hidden" name="defaultDestination" value="@Model.EscrowAddress" />
<input type="hidden" name="defaultAmount" value="@Model.Amount" />
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public class AtomicSwapEscrowViewModel
{
public string ToSend { get; set; }
public string ToReceive { get; internal set; }
public string SentToWalletId { get; set; }
public Controllers.WalletsController.RefundTime RefundTime { get; set; }
public string SentFromWalletId { get; set; }
public string EscrowAddress { get; internal set; }
public string Amount { get; set; }
}
}

View File

@ -0,0 +1,47 @@
@model ListViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage atomic swaps";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
<div class="col-md-10">
<a asp-route-walletId="@this.Context.GetRouteValue("walletId")" asp-action="NewAtomicSwap" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Make a @Model.CryptoCode offer</a>&nbsp;
<a asp-route-walletId="@this.Context.GetRouteValue("walletId")" asp-action="TakeAtomicSwap" class="btn btn-secondary" role="button"><span class="fa fa-sign-in"></span> Take a @Model.CryptoCode offer</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th style="text-align:center">Date</th>
<th style="text-align:center">Partner</th>
<th style="text-align:right">Send</th>
<th style="text-align:right">Receive</th>
<th style="text-align:center">Status</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var swap in Model.Swaps)
{
<tr>
<td>@swap.Timestamp.ToTimeAgo()</td>
<td>@swap.Partner</td>
<td style="text-align:right;">@swap.Sent</td>
<td style="text-align:right;">@swap.Received</td>
<td style="text-align:center">@swap.Status</td>
<td style="text-align:right">
<a asp-action="AtomicSwapDetails" asp-route-walletId="@swap.WalletId" asp-route-offerId="@swap.OfferId">Details</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,151 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.AtomicSwaps;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Views.Wallets
{
public class AtomicSwapRepository
{
ConcurrentDictionary<string, AtomicSwapEntry> _Offers = new ConcurrentDictionary<string, AtomicSwapEntry>();
public Task SaveEntry(WalletId walletId, string offerId, AtomicSwapEntry entry)
{
entry.Id = offerId;
_Offers.TryAdd(offerId, entry);
_OfferIdsByWalletId.Add(walletId, offerId);
return Task.CompletedTask;
}
public Task<AtomicSwapEntry> GetEntry(string offerId)
{
_Offers.TryGetValue(offerId, out var offer);
return Task.FromResult(offer);
}
internal IEnumerable<AtomicSwapEntry> GetEntries(WalletId walletId)
{
if (!_OfferIdsByWalletId.TryGetValue(walletId, out var offers))
return Array.Empty<AtomicSwapEntry>();
return _OfferIdsByWalletId[walletId].Select(c => GetEntry(c).Result).OrderByDescending(o => o.Offer.CreatedAt);
}
MultiValueDictionary<WalletId, string> _OfferIdsByWalletId = new MultiValueDictionary<WalletId, string>();
internal Task UpdateEntry(string offerId, AtomicSwapEntry entry)
{
_Offers.AddOrUpdate(offerId, entry, (k, oldv) => entry);
return Task.CompletedTask;
}
}
public enum XSwapRole
{
Maker,
Taker
}
public enum XSwapStatus
{
WaitingTaker,
WaitingEscrow,
WaitingPeerEscrow,
WaitingBlocks,
CashingOut,
Refunding,
CashedOut,
Refunded
}
public class AtomicSwapEntry
{
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public XSwapRole Role { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public XSwapStatus Status { get; set; }
public AtomicSwapOffer Offer { get; set; }
public SentAtomicSwapAsset Sent { get; set; }
public ReceivedAtomicSwapAsset Received { get; set; }
public string Partner { get; set; }
public Uri OtherUri { get; set; }
public string Id { get; internal set; }
public Preimage Preimage { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt160JsonConverter))]
public uint160 Hash { get; set; }
}
public class AtomicSwapEscrowData
{
public AtomicSwapEscrowData(AtomicSwapOfferAsset offerAsset)
{
Amount = offerAsset.Amount;
LockTime = offerAsset.LockTime;
CryptoCode = offerAsset.CryptoCode;
}
public AtomicSwapEscrowData()
{
}
public string CryptoCode { get; set; }
public Key MyKey { get; set; }
public PubKey OtherKey { get; set; }
public LockTime LockTime { get; set; }
public Money Amount { get; set; }
public WalletId WalletId { get; set; }
public EscrowScriptPubKeyParameters GetEscrow()
{
return new EscrowScriptPubKeyParameters(MyKey.PubKey, OtherKey, LockTime);
}
}
public class ReceivedAtomicSwapAsset : AtomicSwapEscrowData
{
public ReceivedAtomicSwapAsset(AtomicSwapOfferAsset offerAsset) : base(offerAsset)
{
}
public ReceivedAtomicSwapAsset()
{
}
public Script Destination { get; set; }
}
public class SentAtomicSwapAsset : AtomicSwapEscrowData
{
public SentAtomicSwapAsset(AtomicSwapOfferAsset offerAsset) : base(offerAsset)
{
}
public SentAtomicSwapAsset()
{
}
public Script Refund { get; set; }
}
public class AtomicSwapOffer
{
[JsonConverter(typeof(Newtonsoft.Json.Converters.UnixDateTimeConverter))]
public DateTimeOffset CreatedAt { get; set; }
public string Rule { get; set; }
public AtomicSwapOfferAsset Offer { get; set; }
public AtomicSwapOfferAsset Price { get; set; }
public Uri MarketMakerUri { get; set; }
}
public class AtomicSwapOfferAsset
{
public string CryptoCode { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.LockTimeJsonConverter))]
public LockTime LockTime { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
public Money Amount { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyJsonConverter))]
public PubKey Pubkey { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public class ListViewModel
{
public class SwapItem
{
public string OfferId { get; set; }
public string WalletId { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string Partner { get; set; }
public string Sent { get; set; }
public string Received { get; set; }
public string Status { get; set; }
public string Role { get; set; }
}
public string CryptoCode { get; set; }
public List<SwapItem> Swaps { get; set; } = new List<SwapItem>();
}
}

View File

@ -0,0 +1,73 @@
@model NewViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Create new atomic swap offer";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="SelectedWallet" class="control-label"></label>
<select asp-for="SelectedWallet" asp-items="Model.WalletList" class="form-control" onchange="updateCryptoDestination();"></select>
<span asp-validation-for="SelectedWallet" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="RateRule" class="control-label"></label>
<div class="input-group">
<input asp-for="RateRule" class="form-control" />
<div class="input-group-prepend">
<span class="input-group-text" style="display:none;" id="cryptoDestination"></span>
</div>
</div>
<span asp-validation-for="RateRule" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Amount"></label>
<div class="input-group">
<input asp-for="Amount" class="form-control" />
<div class="input-group-prepend">
<span class="input-group-text">@Model.CryptoCode</span>
</div>
</div>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Spread"></label>
<div class="input-group">
<input asp-for="Spread" class="form-control" />
<div class="input-group-prepend">
<span class="input-group-text">%</span>
</div>
</div>
<span asp-validation-for="Spread" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="New offer" class="btn btn-primary" />
</div>
</form>
<a asp-action="AtomicSwapList">Back to List</a>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
$(function () {
updateCryptoDestination();
});
function updateCryptoDestination() {
var val = $("#SelectedWallet").val();
var selected = srvModel.walletData[val];
$("#cryptoDestination").text(selected.cryptoCode + " per " + '@Model.CryptoCode');
$("#RateRule").val(selected.rule);
$("#Spread").val(selected.spread * 100);
$("#cryptoDestination").show();
}
</script>
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.SqlServer.Server;
namespace BTCPayServer.Views.Wallets
{
public class NamedWallet
{
public string Name { get; set; }
public string CryptoCode { get; set; }
public WalletId WalletId { get; set; }
public Rating.RateRule Rule { get; set; }
public decimal Spread { get; set; }
public DerivationSchemeSettings DerivationStrategy { get; set; }
}
public class NewViewModel
{
public class NameWalletObj
{
public string CryptoCode { get; set; }
public decimal Spread { get; set; }
public string Rule { get; set; }
}
[Display(Name = "To wallet")]
public string SelectedWallet { get; set; }
[Display(Name = "Amount to receive in the destination wallet (or rating rule)")]
[Required()]
public string RateRule { get; set; }
[Range(0, double.MaxValue)]
[Display(Name = "Amount to send from this wallet")]
public double Amount { get; set; }
public string CryptoCode { get; set; }
[Display(Name = "Add a spread on exchange rate of ... %")]
[Range(0.0, 100.0)]
public double Spread
{
get;
set;
}
public Dictionary<WalletId, NameWalletObj> WalletData { get; set; }
public SelectList WalletList { get; set; }
public void SetWalletList(NamedWallet[] namedWallet, string selectedWallet)
{
var choices = namedWallet.Select(o => new { Name = o.Name, Value = o.WalletId.ToString() }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == selectedWallet) ?? choices.FirstOrDefault();
WalletList = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedWallet = chosen.Value;
WalletData = namedWallet.ToDictionary(o => o.WalletId, o => new NameWalletObj() { CryptoCode = o.CryptoCode, Rule = o.Rule.ToString(), Spread = o.Spread });
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public class OfferViewModel
{
public string Title { get; set; }
public string EscrowAddress { get; set; }
public string WalletUrl { get; set; }
public string RefundTime { get; set; }
}
}

View File

@ -0,0 +1,24 @@
@model TakeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Take an atomic swap offer";
ViewData.SetActivePageAndTitle(WalletsNavPages.AtomicSwaps);
}
<div class="row">
<div class="col-md-10">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="MakerUri" class="control-label"></label>*
<input asp-for="MakerUri" class="form-control" />
<span asp-validation-for="MakerUri" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Take offer" class="btn btn-primary" />
</div>
</form>
<a asp-action="AtomicSwapList">Back to List</a>
</div>
</div>

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Views.Wallets
{
public class TakeViewModel
{
[Display(Name = "Maker's URI")]
[UriAttribute]
[Required]
public string MakerUri { get; set; }
}
}

View File

@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Wallets
Transactions,
Rescan,
PSBT,
Settings
Settings,
AtomicSwaps
}
}

View File

@ -15,5 +15,6 @@
{
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT" asp-route-walletId="@this.Context.GetRouteValue("walletId")">PSBT</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Settings)" asp-action="WalletSettings" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSettings">Settings</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.AtomicSwaps)" asp-action="AtomicSwapList" asp-route-walletId="@this.Context.GetRouteValue("walletId")">Atomic Swaps</a>
}
</div>