Compare commits
2 Commits
v1.0.3.151
...
feature/at
Author | SHA1 | Date | |
---|---|---|---|
cf496f1d34 | |||
85d9d348f0 |
BTCPayServer.Common
BTCPayServer.Data/Migrations
BTCPayServer.Rating
AvailableRateProvider.cs
Providers
BackgroundFetcherRateProvider.csCachedRateProvider.csCoinAverageRateProvider.csCoinAverageSettings.csCoinGeckoRateProvider.cs
Services
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csChangellyTests.csPaymentRequestTests.csSeleniumTester.csServerTester.csUnitTest1.cs
BTCPayServer
AtomicSwaps
BTCPayServer.csprojControllers
AtomicSwapController.csInvoiceController.UI.csPublicController.csServerController.csWalletsController.csWalletsControllers.XSwap.cs
EscrowScriptBuilder.csHostedServices
Hosting
Models
Properties
Services
Views
Server
Stores
Wallets
AtomicSwapDetails.cshtmlAtomicSwapDetailsMarkerWaitingTaker.cshtmlAtomicSwapDetailsMarkerWaitingTakerViewModel.csAtomicSwapDetailsTakerWaitingTaker.cshtmlAtomicSwapDetailsTakerWaitingTakerViewModel.csAtomicSwapEscrow.cshtmlAtomicSwapEscrowViewModel.csAtomicSwapList.cshtmlAtomicSwapRepository.csListViewModel.csNewAtomicSwap.cshtmlNewViewModel.csOfferViewModel.csTakeAtomicSwap.cshtmlTakeViewModel.csWalletsNavPages.cs_Nav.cshtml
wwwroot/imlegacy
Build
@ -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>()
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -28,10 +28,11 @@ namespace BTCPayServer
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "liquidnetwork",
|
||||
CryptoImagePath = "imlegacy/liquid.png",
|
||||
CryptoImagePath = "imlegacy/liquid.svg",
|
||||
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>()
|
||||
|
@ -23,7 +23,7 @@ namespace BTCPayServer
|
||||
"USDT_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
DisplayName = "Liquid Tether",
|
||||
DisplayName = "Tether USD",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "liquidnetwork",
|
||||
@ -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>()
|
||||
|
@ -22,10 +22,5 @@ namespace BTCPayServer
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public override string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
|
||||
{
|
||||
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,11 +112,6 @@ namespace BTCPayServer
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
|
||||
{
|
||||
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue}";
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BTCPayNetworkBase
|
||||
@ -136,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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@ -11,91 +10,21 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (!migrationBuilder.IsSqlite())
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictTokens",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictTokens",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictAuthorizations",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ReplaceOldTable(migrationBuilder, s =>
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: s,
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
AuthorizationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
|
||||
CreationDate = table.Column<DateTimeOffset>(nullable: true),
|
||||
ExpirationDate = table.Column<DateTimeOffset>(nullable: true),
|
||||
Id = table.Column<string>(nullable: false, maxLength: null),
|
||||
Payload = table.Column<string>(nullable: true),
|
||||
Properties = table.Column<string>(nullable: true),
|
||||
ReferenceId = table.Column<string>(maxLength: 100, nullable: true),
|
||||
Status = table.Column<string>(maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(maxLength: 450, nullable: true),
|
||||
Type = table.Column<string>(maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
|
||||
column: x => x.AuthorizationId,
|
||||
principalTable: "OpenIddictAuthorizations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
}, "OpenIddictTokens");
|
||||
|
||||
ReplaceOldTable(migrationBuilder, s =>
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: s,
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
|
||||
Id = table.Column<string>(nullable: false, maxLength: null),
|
||||
Properties = table.Column<string>(nullable: true),
|
||||
Scopes = table.Column<string>(nullable: true),
|
||||
Status = table.Column<string>(maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(maxLength: 450, nullable: true),
|
||||
Type = table.Column<string>(maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
}, "OpenIddictAuthorizations");
|
||||
}
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictAuthorizations",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Requirements",
|
||||
@ -105,140 +34,27 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (!migrationBuilder.IsSqlite())
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Requirements",
|
||||
table: "OpenIddictApplications");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Requirements",
|
||||
table: "OpenIddictApplications");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictTokens",
|
||||
maxLength: 450,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictTokens",
|
||||
maxLength: 450,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictAuthorizations",
|
||||
maxLength: 450,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOldTable(migrationBuilder, s =>
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: s,
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
AuthorizationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
|
||||
CreationDate = table.Column<DateTimeOffset>(nullable: true),
|
||||
ExpirationDate = table.Column<DateTimeOffset>(nullable: true),
|
||||
Id = table.Column<string>(nullable: false, maxLength: null),
|
||||
Payload = table.Column<string>(nullable: true),
|
||||
Properties = table.Column<string>(nullable: true),
|
||||
ReferenceId = table.Column<string>(maxLength: 100, nullable: true),
|
||||
Status = table.Column<string>(maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(maxLength: 450, nullable: false),
|
||||
Type = table.Column<string>(maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
|
||||
column: x => x.AuthorizationId,
|
||||
principalTable: "OpenIddictAuthorizations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
}, "OpenIddictTokens", "WHERE Subject IS NOT NULL");
|
||||
|
||||
ReplaceOldTable(migrationBuilder, s =>
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: s,
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
|
||||
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
|
||||
Id = table.Column<string>(nullable: false, maxLength: null),
|
||||
Properties = table.Column<string>(nullable: true),
|
||||
Scopes = table.Column<string>(nullable: true),
|
||||
Status = table.Column<string>(maxLength: 25, nullable: false),
|
||||
Subject = table.Column<string>(maxLength: 450, nullable: false),
|
||||
Type = table.Column<string>(maxLength: 25, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
|
||||
column: x => x.ApplicationId,
|
||||
principalTable: "OpenIddictApplications",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
}, "OpenIddictAuthorizations", "WHERE Subject IS NOT NULL");
|
||||
|
||||
ReplaceOldTable(migrationBuilder, s =>
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: s,
|
||||
columns: table => new
|
||||
{
|
||||
ClientId = table.Column<string>(maxLength: 100, nullable: false),
|
||||
ClientSecret = table.Column<string>(nullable: true),
|
||||
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
|
||||
ConsentType = table.Column<string>(nullable: true),
|
||||
DisplayName = table.Column<string>(nullable: true),
|
||||
Id = table.Column<string>(nullable: false, maxLength: null),
|
||||
Permissions = table.Column<string>(nullable: true),
|
||||
PostLogoutRedirectUris = table.Column<string>(nullable: true),
|
||||
Properties = table.Column<string>(nullable: true),
|
||||
RedirectUris = table.Column<string>(nullable: true),
|
||||
Type = table.Column<string>(maxLength: 25, nullable: false),
|
||||
ApplicationUserId = table.Column<string>(nullable: true, maxLength: null)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
}, "OpenIddictApplications", "",
|
||||
"ClientId, ClientSecret, ConcurrencyToken, ConsentType, DisplayName, Id, Permissions, PostLogoutRedirectUris, Properties, RedirectUris, Type, ApplicationUserId");
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceOldTable(MigrationBuilder migrationBuilder, Action<string> createTable, string tableName,
|
||||
string whereClause = "", string columns = "*")
|
||||
{
|
||||
createTable.Invoke($"New_{tableName}");
|
||||
migrationBuilder.Sql(
|
||||
$"INSERT INTO New_{tableName} {(columns == "*" ? string.Empty : $"({columns})")}SELECT {columns} FROM {tableName} {whereClause};");
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys=\"0\"", true);
|
||||
migrationBuilder.Sql($"DROP TABLE {tableName}", true);
|
||||
migrationBuilder.Sql($"ALTER TABLE New_{tableName} RENAME TO {tableName}", true);
|
||||
migrationBuilder.Sql("PRAGMA foreign_keys=\"1\"", true);
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictAuthorizations",
|
||||
maxLength: 450,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ namespace BTCPayServer.Rating
|
||||
public enum RateSource
|
||||
{
|
||||
Coingecko,
|
||||
CoinAverage,
|
||||
Direct
|
||||
}
|
||||
public class AvailableRateProvider
|
||||
@ -10,17 +11,11 @@ namespace BTCPayServer.Rating
|
||||
public string Name { get; }
|
||||
public string Url { get; }
|
||||
public string Id { get; }
|
||||
public string SourceId { get; }
|
||||
public RateSource Source { get; }
|
||||
|
||||
public AvailableRateProvider(string id, string name, string url) : this(id, id, name, url, RateSource.Direct)
|
||||
{
|
||||
|
||||
}
|
||||
public AvailableRateProvider(string id, string sourceId, string name, string url, RateSource source)
|
||||
public AvailableRateProvider(string id, string name, string url, RateSource source)
|
||||
{
|
||||
Id = id;
|
||||
SourceId = sourceId;
|
||||
Name = name;
|
||||
Url = url;
|
||||
Source = source;
|
||||
|
@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider
|
||||
/// This class is a decorator which handle caching and pre-emptive query to the underlying exchange
|
||||
/// </summary>
|
||||
public class BackgroundFetcherRateProvider : IRateProvider
|
||||
{
|
||||
@ -139,9 +139,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
TimeSpan _RefreshRate = TimeSpan.FromSeconds(30);
|
||||
/// <summary>
|
||||
/// The timespan after which <see cref="UpdateIfNecessary(CancellationToken)"/> will get the rates from the underlying rate provider
|
||||
/// </summary>
|
||||
public TimeSpan RefreshRate
|
||||
{
|
||||
get
|
||||
@ -159,9 +156,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
TimeSpan _ValidatyTime = TimeSpan.FromMinutes(10);
|
||||
/// <summary>
|
||||
/// The timespan after which calls to <see cref="GetRatesAsync(CancellationToken)"/> will query underlying provider if the rate has not been updated
|
||||
/// </summary>
|
||||
public TimeSpan ValidatyTime
|
||||
{
|
||||
get
|
||||
|
53
BTCPayServer.Rating/Providers/CachedRateProvider.cs
Normal file
53
BTCPayServer.Rating/Providers/CachedRateProvider.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CachedRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
private IRateProvider _Inner;
|
||||
private IMemoryCache _MemoryCache;
|
||||
|
||||
public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache)
|
||||
{
|
||||
if (inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
if (memoryCache == null)
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
this._Inner = inner;
|
||||
this.MemoryCache = memoryCache;
|
||||
this.ExchangeName = exchangeName;
|
||||
}
|
||||
|
||||
public IRateProvider Inner
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Inner;
|
||||
}
|
||||
}
|
||||
|
||||
public string ExchangeName { get; }
|
||||
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
|
||||
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
195
BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs
Normal file
195
BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CoinAverageException : Exception
|
||||
{
|
||||
public CoinAverageException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class RatesSetting
|
||||
{
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int CacheInMinutes { get; set; } = 15;
|
||||
}
|
||||
|
||||
public interface ICoinAverageAuthenticator
|
||||
{
|
||||
Task AddHeader(HttpRequestMessage message);
|
||||
}
|
||||
|
||||
public class CoinAverageRateProvider : IRateProvider, IHasExchangeName
|
||||
{
|
||||
public const string CoinAverageName = "coinaverage";
|
||||
public CoinAverageRateProvider()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public HttpClient HttpClient
|
||||
{
|
||||
get
|
||||
{
|
||||
return _LocalClient ?? _Client;
|
||||
}
|
||||
set
|
||||
{
|
||||
_LocalClient = value;
|
||||
}
|
||||
}
|
||||
HttpClient _LocalClient;
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
public string Exchange { get; set; } = CoinAverageName;
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public string Market
|
||||
{
|
||||
get; set;
|
||||
} = "global";
|
||||
|
||||
public ICoinAverageAuthenticator Authenticator { get; set; }
|
||||
|
||||
public string ExchangeName => Exchange ?? CoinAverageName;
|
||||
|
||||
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
|
||||
{
|
||||
bidAsk = null;
|
||||
if (Exchange == CoinAverageName)
|
||||
{
|
||||
JToken last = p.Value["last"];
|
||||
if (!decimal.TryParse(last.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) ||
|
||||
v <= 0)
|
||||
return false;
|
||||
bidAsk = new BidAsk(v);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
JToken bid = p.Value["bid"];
|
||||
JToken ask = p.Value["ask"];
|
||||
if (bid == null || ask == null ||
|
||||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
|
||||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
|
||||
v1 > v2 ||
|
||||
v1 <= 0 || v2 <= 0)
|
||||
return false;
|
||||
bidAsk = new BidAsk(v1, v2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
|
||||
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await HttpClient.SendAsync(request, cancellationToken);
|
||||
using (resp)
|
||||
{
|
||||
|
||||
if ((int)resp.StatusCode == 401)
|
||||
throw new CoinAverageException("Unauthorized access to the API");
|
||||
if ((int)resp.StatusCode == 429)
|
||||
throw new CoinAverageException("Exceed API limits");
|
||||
if ((int)resp.StatusCode == 403)
|
||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
if (Exchange != CoinAverageName)
|
||||
{
|
||||
rates = (JObject)rates["symbols"];
|
||||
}
|
||||
|
||||
var exchangeRates = new ExchangeRates();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
ExchangeRate exchangeRate = new ExchangeRate();
|
||||
exchangeRate.Exchange = Exchange;
|
||||
if (!TryToBidAsk(prop, out var value))
|
||||
continue;
|
||||
exchangeRate.BidAsk = value;
|
||||
if (CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
{
|
||||
exchangeRate.CurrencyPair = pair;
|
||||
exchangeRates.Add(exchangeRate);
|
||||
}
|
||||
}
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TestAuthAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await HttpClient.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await HttpClient.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
var response = new GetRateLimitsResponse();
|
||||
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
|
||||
var totalPeriod = jobj["total_period"].Value<string>();
|
||||
if (totalPeriod == "24h")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromHours(24);
|
||||
}
|
||||
else if (totalPeriod == "30d")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromDays(30);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
|
||||
}
|
||||
response.RequestsLeft = jobj["requests_left"].Value<int>();
|
||||
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public class GetRateLimitsResponse
|
||||
{
|
||||
public TimeSpan CounterReset { get; set; }
|
||||
public int RequestsLeft { get; set; }
|
||||
public int RequestsPerPeriod { get; set; }
|
||||
public TimeSpan TotalPeriod { get; set; }
|
||||
}
|
||||
}
|
51
BTCPayServer.Rating/Providers/CoinAverageSettings.cs
Normal file
51
BTCPayServer.Rating/Providers/CoinAverageSettings.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
|
||||
{
|
||||
CoinAverageSettings _Settings;
|
||||
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
|
||||
{
|
||||
_Settings = settings;
|
||||
}
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
return _Settings.AddHeader(message);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
|
||||
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var signature = GetCoinAverageSignature();
|
||||
if (signature != null)
|
||||
{
|
||||
message.Headers.Add("X-signature", signature);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetCoinAverageSignature()
|
||||
{
|
||||
var keyPair = KeyPair;
|
||||
if (!keyPair.HasValue)
|
||||
return null;
|
||||
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
|
||||
return null;
|
||||
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
|
||||
var payload = timestamp + "." + keyPair.Value.PublicKey;
|
||||
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
|
||||
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
|
||||
return payload + "." + digestValueHex;
|
||||
}
|
||||
}
|
||||
}
|
@ -18,14 +18,6 @@ namespace BTCPayServer.Services.Rates
|
||||
public string Exchange { get; set; }
|
||||
public string ExchangeName => Exchange ?? CoinGeckoName;
|
||||
|
||||
public bool CoinGeckoRate
|
||||
{
|
||||
get
|
||||
{
|
||||
return ExchangeName == CoinGeckoName;
|
||||
}
|
||||
}
|
||||
|
||||
public CoinGeckoRateProvider(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
if (httpClientFactory == null)
|
||||
@ -39,12 +31,12 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public virtual Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return CoinGeckoRate ? GetCoinGeckoRates(cancellationToken) : GetCoinGeckoExchangeSpecificRates(1, cancellationToken);
|
||||
return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates();
|
||||
}
|
||||
|
||||
private async Task<ExchangeRates> GetCoinGeckoRates(CancellationToken cancellationToken)
|
||||
private async Task<ExchangeRates> GetCoinGeckoRates()
|
||||
{
|
||||
using var resp = await GetWithBackoffAsync("exchange_rates", cancellationToken);
|
||||
using var resp = await Client.GetAsync("exchange_rates");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return new ExchangeRates(JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children()
|
||||
.Where(token => ((JProperty)token).Name != "btc")
|
||||
@ -53,28 +45,9 @@ namespace BTCPayServer.Services.Rates
|
||||
new BidAsk(((JProperty)token).Value["value"].Value<decimal>()))));
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> GetWithBackoffAsync(string request, CancellationToken cancellationToken)
|
||||
private async Task<ExchangeRates> GetCoinGeckoExchangeSpecificRates(int page = 1)
|
||||
{
|
||||
TimeSpan retryWait = TimeSpan.FromSeconds(1);
|
||||
retry:
|
||||
var resp = await Client.GetAsync(request, cancellationToken);
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
resp.Dispose();
|
||||
if (retryWait < TimeSpan.FromSeconds(60))
|
||||
{
|
||||
await Task.Delay(retryWait, cancellationToken);
|
||||
retryWait = TimeSpan.FromSeconds(retryWait.TotalSeconds * 2);
|
||||
goto retry;
|
||||
}
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
private async Task<ExchangeRates> GetCoinGeckoExchangeSpecificRates(int page, CancellationToken cancellationToken)
|
||||
{
|
||||
using var resp = await GetWithBackoffAsync($"exchanges/{Exchange}/tickers?page={page}", cancellationToken);
|
||||
using var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}");
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
List<ExchangeRate> result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers")
|
||||
@ -96,7 +69,7 @@ retry:
|
||||
var tasks = new List<Task<ExchangeRates>>();
|
||||
for (int i = 2; i <= totalPages; i++)
|
||||
{
|
||||
tasks.Add(GetCoinGeckoExchangeSpecificRates(i, cancellationToken));
|
||||
tasks.Add(GetCoinGeckoExchangeSpecificRates(i));
|
||||
}
|
||||
|
||||
foreach (var t in (await Task.WhenAll(tasks)))
|
||||
|
@ -49,13 +49,18 @@ namespace BTCPayServer.Services.Rates
|
||||
public ExchangeRates ExchangeRates { get; set; }
|
||||
public ExchangeException Exception { get; internal set; }
|
||||
}
|
||||
public RateProviderFactory(IHttpClientFactory httpClientFactory)
|
||||
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_CoinAverageSettings = coinAverageSettings;
|
||||
_CacheOptions = cacheOptions;
|
||||
// We use 15 min because of limits with free version of bitcoinaverage
|
||||
CacheSpan = TimeSpan.FromMinutes(15.0);
|
||||
InitExchanges();
|
||||
}
|
||||
private IOptions<MemoryCacheOptions> _CacheOptions;
|
||||
TimeSpan _CacheSpan;
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
@ -71,12 +76,19 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public void InvalidateCache()
|
||||
{
|
||||
var cache = new MemoryCache(_CacheOptions);
|
||||
foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null))
|
||||
{
|
||||
provider.CacheSpan = CacheSpan;
|
||||
provider.MemoryCache = cache;
|
||||
}
|
||||
if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
|
||||
{
|
||||
c.RefreshRate = CacheSpan;
|
||||
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
|
||||
}
|
||||
}
|
||||
CoinAverageSettings _CoinAverageSettings;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
|
||||
public Dictionary<string, IRateProvider> Providers
|
||||
@ -88,17 +100,18 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
|
||||
{
|
||||
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr");
|
||||
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries");
|
||||
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker");
|
||||
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker");
|
||||
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker");
|
||||
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker", RateSource.Direct);
|
||||
|
||||
yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates");
|
||||
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
|
||||
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
|
||||
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
|
||||
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
|
||||
yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates", RateSource.Direct);
|
||||
yield return new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", "https://apiv2.bitcoinaverage.com/indices/global/ticker/short", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices", RateSource.Direct);
|
||||
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates", RateSource.Direct);
|
||||
}
|
||||
void InitExchanges()
|
||||
{
|
||||
@ -111,6 +124,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
// Handmade providers
|
||||
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory));
|
||||
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
|
||||
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
|
||||
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
|
||||
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
|
||||
@ -124,24 +138,51 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
foreach (var provider in Providers.ToArray())
|
||||
{
|
||||
if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs
|
||||
continue;
|
||||
var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]);
|
||||
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
if (provider.Key == CoinGeckoRateProvider.CoinGeckoName)
|
||||
{
|
||||
prov.RefreshRate = CacheSpan;
|
||||
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
}
|
||||
Providers[provider.Key] = prov;
|
||||
}
|
||||
|
||||
var cache = new MemoryCache(_CacheOptions);
|
||||
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
|
||||
{
|
||||
if (!Providers.ContainsKey(supportedExchange.Id))
|
||||
{
|
||||
var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
{
|
||||
Exchange = supportedExchange.Id
|
||||
};
|
||||
var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko);
|
||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
|
||||
{
|
||||
CacheSpan = CacheSpan
|
||||
};
|
||||
Providers.Add(supportedExchange.Id, cached);
|
||||
}
|
||||
}
|
||||
foreach (var supportedExchange in GetCoinAverageSupportedExchanges())
|
||||
{
|
||||
if (!Providers.ContainsKey(supportedExchange.Id))
|
||||
{
|
||||
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
{
|
||||
Exchange = supportedExchange.Id
|
||||
};
|
||||
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
|
||||
{
|
||||
CacheSpan = CacheSpan
|
||||
};
|
||||
Providers.Add(supportedExchange.Id, cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,17 +201,91 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
availableProviders.TryAdd(exchange.Id, exchange);
|
||||
}
|
||||
foreach (var exchange in GetCoinAverageSupportedExchanges())
|
||||
{
|
||||
availableProviders.TryAdd(exchange.Id, exchange);
|
||||
}
|
||||
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
|
||||
}
|
||||
return _AvailableRateProviders;
|
||||
}
|
||||
|
||||
internal IEnumerable<AvailableRateProvider> GetCoinAverageSupportedExchanges()
|
||||
{
|
||||
foreach (var item in
|
||||
new[] {
|
||||
(DisplayName: "Idex", Name: "idex"),
|
||||
(DisplayName: "Coinfloor", Name: "coinfloor"),
|
||||
(DisplayName: "Okex", Name: "okex"),
|
||||
(DisplayName: "Bitfinex", Name: "bitfinex"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||
(DisplayName: "Kucoin", Name: "kucoin"),
|
||||
(DisplayName: "IDAX", Name: "idax"),
|
||||
(DisplayName: "Kraken", Name: "kraken"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
|
||||
(DisplayName: "CEX.IO", Name: "cex"),
|
||||
(DisplayName: "Bitex.la", Name: "bitex"),
|
||||
(DisplayName: "Quoine", Name: "quoine"),
|
||||
(DisplayName: "Stex", Name: "stex"),
|
||||
(DisplayName: "CoinTiger", Name: "cointiger"),
|
||||
(DisplayName: "Poloniex", Name: "poloniex"),
|
||||
(DisplayName: "Zaif", Name: "zaif"),
|
||||
(DisplayName: "Huobi", Name: "huobi"),
|
||||
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
|
||||
(DisplayName: "Tidex", Name: "tidex"),
|
||||
(DisplayName: "Tokenomy", Name: "tokenomy"),
|
||||
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
|
||||
(DisplayName: "Kryptono", Name: "kryptono"),
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
(DisplayName: "Korbit", Name: "korbit"),
|
||||
(DisplayName: "Yobit", Name: "yobit"),
|
||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||
(DisplayName: "Livecoin", Name: "livecoin"),
|
||||
(DisplayName: "Hotbit", Name: "hotbit"),
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Binance", Name: "binance"),
|
||||
(DisplayName: "Bit-Z", Name: "bitz"),
|
||||
(DisplayName: "Coinbase Pro", Name: "coinbasepro"),
|
||||
(DisplayName: "Rock Trading", Name: "rocktrading"),
|
||||
(DisplayName: "Bittrex", Name: "bittrex"),
|
||||
(DisplayName: "BitBay", Name: "bitbay"),
|
||||
(DisplayName: "Tokenize", Name: "tokenize"),
|
||||
(DisplayName: "Hitbtc", Name: "hitbtc"),
|
||||
(DisplayName: "Upbit", Name: "upbit"),
|
||||
(DisplayName: "Bitstamp", Name: "bitstamp"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Trade.io", Name: "tradeio"),
|
||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||
(DisplayName: "Independent Reserve", Name: "independentreserve"),
|
||||
(DisplayName: "Coinsquare", Name: "coinsquare"),
|
||||
(DisplayName: "Exmoney", Name: "exmoney"),
|
||||
(DisplayName: "Coinegg", Name: "coinegg"),
|
||||
(DisplayName: "FYB-SG", Name: "fybsg"),
|
||||
(DisplayName: "Cryptonit", Name: "cryptonit"),
|
||||
(DisplayName: "BTCTurk", Name: "btcturk"),
|
||||
(DisplayName: "bitFlyer", Name: "bitflyer"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
(DisplayName: "OasisDEX", Name: "oasisdex"),
|
||||
(DisplayName: "CoinMate", Name: "coinmate"),
|
||||
(DisplayName: "BitForex", Name: "bitforex"),
|
||||
(DisplayName: "Bitsquare", Name: "bitsquare"),
|
||||
(DisplayName: "FYB-SE", Name: "fybse"),
|
||||
(DisplayName: "itBit", Name: "itbit"),
|
||||
})
|
||||
{
|
||||
yield return new AvailableRateProvider(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}", RateSource.CoinAverage);
|
||||
}
|
||||
yield return new AvailableRateProvider("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/gdax", RateSource.CoinAverage);
|
||||
}
|
||||
|
||||
internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
|
||||
{
|
||||
return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
|
||||
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["id"].ToString().ToLowerInvariant(), token["name"].ToString(),
|
||||
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["name"].ToString(),
|
||||
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko))
|
||||
.Concat(new[] { new AvailableRateProvider("gdax", "gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
|
||||
.Concat(new[] { new AvailableRateProvider("gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
|
||||
}
|
||||
|
||||
private string Normalize(string name)
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
@ -7,17 +7,7 @@
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
|
||||
<ItemGroup>
|
||||
<DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
|
||||
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -862,7 +863,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
|
||||
// Hijack the test to see if we can add label and comments
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello-pouet"));
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test"));
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2"));
|
||||
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||
@ -1489,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")]
|
||||
@ -2684,12 +2782,16 @@ noninventoryitem:
|
||||
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
|
||||
foreach (var result in factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider bf && !(bf.Inner is CoinGeckoRateProvider cg && !cg.CoinGeckoRate))
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider)
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.ToList())
|
||||
{
|
||||
|
||||
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
|
||||
if (result.ExpectedName == "quadrigacx")
|
||||
continue; // 29 january, the exchange is down
|
||||
if (result.ExpectedName == "coinaverage")
|
||||
continue; // no more free plan
|
||||
result.Fetcher.InvalidateCache();
|
||||
var exchangeRates = result.ResultAsync.Result;
|
||||
result.Fetcher.InvalidateCache();
|
||||
@ -2782,7 +2884,12 @@ noninventoryitem:
|
||||
|
||||
public static RateProviderFactory CreateBTCPayRateFactory()
|
||||
{
|
||||
return new RateProviderFactory(new MockHttpClientFactory());
|
||||
return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
|
||||
}
|
||||
|
||||
private static MemoryCacheOptions CreateMemoryCache()
|
||||
{
|
||||
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
|
||||
}
|
||||
|
||||
class SpyRateProvider : IRateProvider
|
||||
@ -2886,17 +2993,42 @@ noninventoryitem:
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var spy = new SpyRateProvider();
|
||||
RateRules.TryParse("X_X = bittrex(X_X);", out var rateRules);
|
||||
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
factory.Providers.Clear();
|
||||
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
|
||||
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
|
||||
factory.CacheSpan = TimeSpan.FromSeconds(1);
|
||||
|
||||
var fetcher = new RateFetcher(factory);
|
||||
|
||||
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
|
||||
Thread.Sleep(3000);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
// Should cache at exchange level so this should hit the cache
|
||||
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
Assert.Null(fetchedRate2.BidAsk);
|
||||
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
|
||||
|
||||
// Should cache at exchange level this should not hit the cache as it is different exchange
|
||||
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
|
||||
factory.Providers.Clear();
|
||||
var fetch = new BackgroundFetcherRateProvider("spy", spy);
|
||||
fetch.DoNotAutoFetchIfExpired = true;
|
||||
factory.Providers.Add("bittrex", fetch);
|
||||
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertHit();
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
spy.AssertNotHit();
|
||||
|
130
BTCPayServer/AtomicSwaps/AtomicSwapClient.cs
Normal file
130
BTCPayServer/AtomicSwaps/AtomicSwapClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
17
BTCPayServer/AtomicSwaps/AtomicSwapTakeRequest.cs
Normal file
17
BTCPayServer/AtomicSwaps/AtomicSwapTakeRequest.cs
Normal 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; }
|
||||
}
|
||||
}
|
16
BTCPayServer/AtomicSwaps/AtomicSwapTakeResponse.cs
Normal file
16
BTCPayServer/AtomicSwaps/AtomicSwapTakeResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
31
BTCPayServer/AtomicSwaps/Preimage.cs
Normal file
31
BTCPayServer/AtomicSwaps/Preimage.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,137 +1,135 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<RazorCompileOnBuild>false</RazorCompileOnBuild>
|
||||
</PropertyGroup>
|
||||
<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>
|
||||
@ -160,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>
|
||||
@ -169,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>
|
||||
|
97
BTCPayServer/Controllers/AtomicSwapController.cs
Normal file
97
BTCPayServer/Controllers/AtomicSwapController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
|
||||
return new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId),
|
||||
IsLightning =
|
||||
kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
|
@ -49,27 +49,17 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
DataWrapper<InvoiceResponse> invoice = null;
|
||||
try
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
NotificationEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
ModelState.AddModelError("Store", e.Message);
|
||||
return View();
|
||||
}
|
||||
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
ItemDesc = model.CheckoutDesc,
|
||||
OrderId = model.OrderId,
|
||||
NotificationEmail = model.NotifyEmail,
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
if (string.IsNullOrEmpty(model.CheckoutQueryString))
|
||||
{
|
||||
return Redirect(invoice.Data.Url);
|
||||
|
@ -84,6 +84,76 @@ namespace BTCPayServer.Controllers
|
||||
_sshState = sshState;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
public async Task<IActionResult> Rates()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
|
||||
var vm = new RatesViewModel()
|
||||
{
|
||||
CacheMinutes = rates.CacheInMinutes,
|
||||
PrivateKey = rates.PrivateKey,
|
||||
PublicKey = rates.PublicKey
|
||||
};
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static async Task FetchRateLimits(RatesViewModel vm)
|
||||
{
|
||||
var coinAverage = GetCoinaverageService(vm, false);
|
||||
if (coinAverage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Rates(RatesViewModel vm)
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
rates.PrivateKey = vm.PrivateKey;
|
||||
rates.PublicKey = vm.PublicKey;
|
||||
rates.CacheInMinutes = vm.CacheMinutes;
|
||||
try
|
||||
{
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if (service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
await _SettingsRepository.UpdateSetting(rates);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Rate settings successfully updated";
|
||||
return RedirectToAction(nameof(Rates));
|
||||
}
|
||||
|
||||
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
|
||||
{
|
||||
var settings = new CoinAverageSettings()
|
||||
{
|
||||
KeyPair = (vm.PublicKey, vm.PrivateKey)
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider()
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
public IActionResult ListUsers(int skip = 0, int count = 50)
|
||||
{
|
||||
|
@ -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
|
||||
|
398
BTCPayServer/Controllers/WalletsControllers.XSwap.cs
Normal file
398
BTCPayServer/Controllers/WalletsControllers.XSwap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
114
BTCPayServer/EscrowScriptBuilder.cs
Normal file
114
BTCPayServer/EscrowScriptBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -29,11 +29,14 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
RateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
RateProviderFactory rateProviderFactory)
|
||||
RateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
}
|
||||
|
||||
@ -41,6 +44,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
return new Task[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSettings),
|
||||
CreateLoopTask(RefreshRates)
|
||||
};
|
||||
}
|
||||
@ -138,5 +142,20 @@ namespace BTCPayServer.HostedServices
|
||||
.ToList();
|
||||
await _SettingsRepository.UpdateSetting(cache);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -97,6 +100,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<U2FService>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -274,7 +278,7 @@ namespace BTCPayServer.Hosting
|
||||
.MinimumLevel.Is(BTCPayServerOptions.GetDebugLogLevel(configuration))
|
||||
.WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1)
|
||||
.CreateLogger();
|
||||
logBuilder.AddProvider(new Serilog.Extensions.Logging.SerilogLoggerProvider(Log.Logger));
|
||||
logBuilder.AddSerilog(Serilog.Log.Logger);
|
||||
}
|
||||
});
|
||||
return services;
|
||||
|
20
BTCPayServer/Models/ServerViewModels/RatesViewModel.cs
Normal file
20
BTCPayServer/Models/ServerViewModels/RatesViewModel.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class RatesViewModel
|
||||
{
|
||||
[Display(Name = "Bitcoin average api keys")]
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
[Display(Name = "Cache the rates for ... minutes")]
|
||||
[Range(0, 60)]
|
||||
public int CacheMinutes { get; set; }
|
||||
public GetRateLimitsResponse RateLimits { get; internal set; }
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? CoinGeckoRateProvider.CoinGeckoName;
|
||||
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.SourceId, GetName(a), a.Url, a.Source)).ToArray();
|
||||
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, GetName(a), a.Url, a.Source)).ToArray();
|
||||
var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
|
||||
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Id;
|
||||
@ -33,7 +33,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
case Rating.RateSource.Direct:
|
||||
return a.Name;
|
||||
case Rating.RateSource.Coingecko:
|
||||
return $"{a.Name} (via CoinGecko)";
|
||||
return $"{a.Name} (via CoinGecko, free)";
|
||||
case Rating.RateSource.CoinAverage:
|
||||
return $"{a.Name} (via BitcoinAverage, commercial)";
|
||||
default:
|
||||
throw new NotSupportedException(a.Source.ToString());
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_BUNDLEJSCSS": "false",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
|
||||
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
|
||||
@ -34,7 +34,7 @@
|
||||
"BTCPAY_BUNDLEJSCSS": "false",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_LBTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
|
||||
|
23
BTCPayServer/Services/AtomicSwapClientFactory.cs
Normal file
23
BTCPayServer/Services/AtomicSwapClientFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -461,6 +461,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
else if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var scheme = ((BTCPayNetwork)info.Network).UriScheme;
|
||||
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate
|
||||
@ -468,7 +470,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP21 = ((BTCPayNetwork)info.Network).GenerateBIP21(cryptoInfo.Address, cryptoInfo.Due),
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
|
||||
#pragma warning disable 618
|
||||
|
@ -85,15 +85,7 @@ namespace BTCPayServer.Services
|
||||
catch (DbUpdateException) // Does not exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch(DbUpdateException) // the Wallet does not exists in the DB
|
||||
{
|
||||
await SetWalletInfo(walletId, new WalletBlobInfo());
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
BTCPayServer/Views/Server/Rates.cshtml
Normal file
61
BTCPayServer/Views/Server/Rates.cshtml
Normal file
@ -0,0 +1,61 @@
|
||||
@model BTCPayServer.Models.ServerViewModels.RatesViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
|
||||
}
|
||||
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<h5>Bitcoin Average</h5>
|
||||
<p>BTCPay relies on Bitcoin Average for getting crypto-currency to fiat rates</p>
|
||||
<p>If you want BTCPay's rate cache to be smaller than 15min, you should register to BitcoinAverage and get a paid API Key.</p>
|
||||
<p>If your BTCPay server instance supports many merchants or is used a lot, BitcoinAverage will rate limit your server and invoices will be created using less accurate rates. (From Bitpay)<br /></p>
|
||||
</div>
|
||||
<form method="post">
|
||||
<label asp-for="PublicKey"></label>
|
||||
<div class="form-inline">
|
||||
<input asp-for="PublicKey" style="width:50%;" class="form-control" placeholder="Public key" />
|
||||
<label class="sr-only" asp-for="PrivateKey"></label>
|
||||
<input asp-for="PrivateKey" style="width:50%;" class="form-control" placeholder="Private key" />
|
||||
<p class="form-text text-muted">You can find the information on the <a target="_blank" href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CacheMinutes"></label>
|
||||
<input asp-for="CacheMinutes" class="form-control" />
|
||||
<span asp-validation-for="CacheMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
@if(Model.RateLimits != null)
|
||||
{
|
||||
<h5>Current Bitcoin Average Quotas:</h5>
|
||||
<table class="table table-sm removetopborder">
|
||||
<tr>
|
||||
<th>Quota period</th>
|
||||
<td>@Model.RateLimits.TotalPeriod.TimeString()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Requests quota</th>
|
||||
<td>@Model.RateLimits.RequestsLeft/@Model.RateLimits.RequestsPerPeriod</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quota reset in</th>
|
||||
<td>@Model.RateLimits.CounterReset.TimeString()</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
|
||||
{
|
||||
public enum ServerNavPages
|
||||
{
|
||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files
|
||||
Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs, Files
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="Users">Users</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||
|
@ -67,7 +67,7 @@
|
||||
</table>
|
||||
<p>Note that the <code>certthumbprint</code> to connect to your LND node can be obtained through this command line:</p>
|
||||
<p><pre><code>openssl x509 -noout -fingerprint -sha256 -inform pem -in /root/.lnd/tls.cert | sed -e 's/.*=//' -e 's/://g'</code></pre></p>
|
||||
<p>You can omit <code>certthumbprint</code> if the certificate is trusted by your machine</p>
|
||||
<p>You can omit <code>certthumbprint</code> if you the certificate is trusted by your machine</p>
|
||||
<p>You can set <code>allowinsecure</code> to <code>true</code> if your LND REST server is using HTTP or HTTPS with an untrusted certificate which you don't know the <code>certthumbprint</code></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -55,6 +55,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header" id="coinaverage-header">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#coinaverage-content" aria-expanded="true">
|
||||
CoinAverage integration (commercial API)
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="coinaverage-content" class="collapse">
|
||||
<div class="card-body text-muted overflow-auto">
|
||||
@foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.CoinAverage))
|
||||
{
|
||||
<a href="@exchange.Url">@exchange.Id</a><span> </span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
21
BTCPayServer/Views/Wallets/AtomicSwapDetails.cshtml
Normal file
21
BTCPayServer/Views/Wallets/AtomicSwapDetails.cshtml
Normal 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>*@
|
@ -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>
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Views/Wallets/AtomicSwapEscrow.cshtml
Normal file
25
BTCPayServer/Views/Wallets/AtomicSwapEscrow.cshtml
Normal 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>
|
18
BTCPayServer/Views/Wallets/AtomicSwapEscrowViewModel.cs
Normal file
18
BTCPayServer/Views/Wallets/AtomicSwapEscrowViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
47
BTCPayServer/Views/Wallets/AtomicSwapList.cshtml
Normal file
47
BTCPayServer/Views/Wallets/AtomicSwapList.cshtml
Normal 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>
|
||||
<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>
|
151
BTCPayServer/Views/Wallets/AtomicSwapRepository.cs
Normal file
151
BTCPayServer/Views/Wallets/AtomicSwapRepository.cs
Normal 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; }
|
||||
}
|
||||
}
|
24
BTCPayServer/Views/Wallets/ListViewModel.cs
Normal file
24
BTCPayServer/Views/Wallets/ListViewModel.cs
Normal 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>();
|
||||
}
|
||||
}
|
73
BTCPayServer/Views/Wallets/NewAtomicSwap.cshtml
Normal file
73
BTCPayServer/Views/Wallets/NewAtomicSwap.cshtml
Normal 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>
|
||||
}
|
62
BTCPayServer/Views/Wallets/NewViewModel.cs
Normal file
62
BTCPayServer/Views/Wallets/NewViewModel.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/Views/Wallets/OfferViewModel.cs
Normal file
16
BTCPayServer/Views/Wallets/OfferViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
24
BTCPayServer/Views/Wallets/TakeAtomicSwap.cshtml
Normal file
24
BTCPayServer/Views/Wallets/TakeAtomicSwap.cshtml
Normal 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>
|
18
BTCPayServer/Views/Wallets/TakeViewModel.cs
Normal file
18
BTCPayServer/Views/Wallets/TakeViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Wallets
|
||||
Transactions,
|
||||
Rescan,
|
||||
PSBT,
|
||||
Settings
|
||||
Settings,
|
||||
AtomicSwaps
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 339.43 295.27"><title>tether-usdt-logo</title><path d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z" style="fill:#50af95;fill-rule:evenodd"/><path d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z" style="fill:#fff;fill-rule:evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000" width="2000" height="2000"><path d="M1000,0c552.26,0,1000,447.74,1000,1000S1552.24,2000,1000,2000,0,1552.38,0,1000,447.68,0,1000,0" fill="#53ae94"/><path d="M1123.42,866.76V718H1463.6V491.34H537.28V718H877.5V866.64C601,879.34,393.1,934.1,393.1,999.7s208,120.36,484.4,133.14v476.5h246V1132.8c276-12.74,483.48-67.46,483.48-133s-207.48-120.26-483.48-133m0,225.64v-0.12c-6.94.44-42.6,2.58-122,2.58-63.48,0-108.14-1.8-123.88-2.62v0.2C633.34,1081.66,451,1039.12,451,988.22S633.36,894.84,877.62,884V1050.1c16,1.1,61.76,3.8,124.92,3.8,75.86,0,114-3.16,121-3.8V884c243.8,10.86,425.72,53.44,425.72,104.16s-182,93.32-425.72,104.18" fill="#fff"/></svg>
|
Before (image error) Size: 874 B After (image error) Size: 704 B |
Binary file not shown.
Before ![]() (image error) Size: 28 KiB |
34
BTCPayServer/wwwroot/imlegacy/liquid.svg
Normal file
34
BTCPayServer/wwwroot/imlegacy/liquid.svg
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>nav_liquid</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<polygon id="path-1" points="0.0158064742 0.0331955923 14.9692374 0.0331955923 14.9692374 23.1732438 0.0158064742 23.1732438"></polygon>
|
||||
<polygon id="path-3" points="0 0.00852524856 15.3983916 0.00852524856 15.3983916 24.9311295 0 24.9311295"></polygon>
|
||||
</defs>
|
||||
<g id="nav_liquid" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-7">
|
||||
<circle id="Oval" fill="#0E726B" fill-rule="nonzero" cx="23" cy="23" r="23"></circle>
|
||||
<g id="Group-13" transform="translate(8.000000, 8.000000)">
|
||||
<path d="M6.89588843,9.41403581 C6.2560124,10.6619008 5.85174242,11.995854 5.69430441,13.3789807 C5.64210055,13.8386915 5.97364325,14.2552893 6.43376722,14.3077686 C6.46613636,14.311281 6.4982989,14.3131405 6.52956612,14.3131405 C6.53858815,14.3131405 6.54754132,14.3129339 6.55642562,14.3126584 C6.96896006,14.2992287 7.31524105,13.9793939 7.36200413,13.5686501 C7.49623278,12.3875207 7.84189394,11.2473003 8.38927686,10.1796694 C9.59113636,7.83476584 11.6381749,6.09695592 14.1532576,5.28641873 C16.6684091,4.47588154 19.3447176,4.69158402 21.6893457,5.89358127 C22.1315634,6.12030303 22.5589738,6.38118457 22.9598691,6.66926997 C23.3358333,6.93869146 23.8611777,6.85260331 24.1308747,6.47725895 C24.4007782,6.10143251 24.314759,5.57608815 23.9390014,5.3061157 C23.4701997,4.96947658 22.9709573,4.66472452 22.4551171,4.40026171 C16.7832851,1.49282369 9.80339532,3.74213499 6.89588843,9.41403581" id="Fill-1" fill="#FFFFFF"></path>
|
||||
<g id="Group-5" transform="translate(2.479339, 0.035675)">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Clip-4"></g>
|
||||
<path d="M2.68554408,22.6742769 C2.9097865,22.9886708 3.26977273,23.1732438 3.65475895,23.1732438 C3.66777548,23.1732438 3.68079201,23.1730372 3.69387741,23.172624 C3.92803719,23.1649793 4.1532438,23.0887397 4.34525482,22.9518939 C4.87934573,22.5708333 5.00386364,21.8262741 4.62280303,21.2923209 C2.31178375,18.0525826 1.76130165,13.8956956 3.15028237,10.1728306 C4.83148072,5.66532369 9.02376722,2.61897383 13.8305165,2.41167355 C14.4860262,2.38343664 14.9963567,1.82716942 14.9681198,1.17165978 C14.9401584,0.52303719 14.3899518,0.0144283747 13.7404339,0.0337121212 L13.7281061,0.0341942149 C7.96412534,0.282679063 2.93685262,3.93591598 0.920599174,9.34108127 C-0.744896694,13.8057507 -0.0851170799,18.790186 2.68554408,22.6742769" id="Fill-3" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
<g id="Group-8" transform="translate(0.000000, 5.889669)">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="Clip-7"></g>
|
||||
<path d="M14.6429477,23.2208678 C8.29411846,22.5359504 3.19763085,17.7794766 1.96092287,11.3850551 C1.29294766,7.93181818 1.85782369,4.34586777 3.55169421,1.28774105 C3.77903581,0.877203857 3.63564738,0.353719008 3.23206612,0.120798898 C3.03674931,0.00805785124 2.80975207,-0.0210055096 2.59336088,0.0391873278 C2.37428375,0.1 2.19232782,0.244214876 2.08096419,0.445316804 C0.190399449,3.85853994 -0.440247934,7.86060606 0.305206612,11.7141185 C1.68557851,18.8517906 7.37655647,24.1613636 14.4666391,24.9265152 C14.4958402,24.9296143 14.5255234,24.9311295 14.5552755,24.9311295 C14.5647107,24.9311295 14.574146,24.9309917 14.5835813,24.9306474 C15.0021074,24.9169421 15.3504545,24.5870523 15.3937741,24.1633609 C15.4421212,23.693595 15.1052755,23.2708678 14.6429477,23.2208678" id="Fill-6" fill="#FFFFFF" mask="url(#mask-4)"></path>
|
||||
</g>
|
||||
<path d="M20.8919766,26.3926377 C19.749759,26.7185331 18.5809573,26.888781 17.4175275,26.8986295 C14.4131887,26.9374036 11.5654614,25.9904339 9.18729339,24.1626102 C9.00988292,24.0258333 8.78977273,23.9663292 8.56766529,23.9952548 C8.34514463,24.0241804 8.14727961,24.1382989 8.01043388,24.3163981 C7.7286157,24.6832025 7.79769284,25.2109573 8.16449725,25.4929132 C10.7931474,27.5135744 13.9281336,28.5781749 17.2434917,28.5781749 C17.3085055,28.5781749 17.3740014,28.5778306 17.4391529,28.5770041 C17.539084,28.575489 17.6386708,28.5732163 17.7383264,28.5700482 L17.7383953,28.5700482 C18.965668,28.5301722 20.1815771,28.3406405 21.352376,28.0067562 C21.7973485,27.879759 22.0560262,27.4144008 21.9290978,26.9694284 C21.8028581,26.5263843 21.3379132,26.2675689 20.8919766,26.3926377" id="Fill-9" fill="#FFFFFF"></path>
|
||||
<path d="M24.8857576,23.2102686 C24.7049036,22.8923623 24.2990496,22.7803788 23.9807989,22.9608196 C22.2428512,23.9473209 19.8861708,24.5384366 17.3449862,24.6251446 C16.9661295,24.6380234 16.6778375,24.9022796 16.659449,25.2535193 C16.6428512,25.5714256 16.8549725,25.8571006 17.1637879,25.9327204 L17.1638567,25.9327204 C17.203595,25.9425 17.2449862,25.9486983 17.2877548,25.9511088 C17.4449862,25.9593044 17.6055923,25.9633678 17.7691598,25.9633678 C19.9498072,25.9633678 22.6442287,25.2461501 24.6361019,24.1155028 C24.7903719,24.0280372 24.9012534,23.8856818 24.9484298,23.7146074 C24.9955372,23.5437397 24.973292,23.3646763 24.8857576,23.2102686" id="Fill-11" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After (image error) Size: 5.5 KiB |
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.3.151</Version>
|
||||
<Version>1.0.3.150</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
Reference in New Issue
Block a user