Compare commits
36 Commits
v1.0.3.148
...
v1.0.3.151
Author | SHA1 | Date | |
---|---|---|---|
605a0fd3c9 | |||
90ec416125 | |||
827b6085af | |||
ff11e6e032 | |||
9b55648e41 | |||
6dffbbd93d | |||
7a0991d6b1 | |||
9b165de5e6 | |||
1b9a4e7775 | |||
48799562f8 | |||
7d545ca682 | |||
9739f3fb25 | |||
bb12de8945 | |||
feabeafed9 | |||
6b427e99ca | |||
31db34ec8d | |||
bf614cd322 | |||
9410933e1c | |||
c269dee980 | |||
5aefb585e9 | |||
780cf67a1b | |||
12e7c5e5e1 | |||
92e5f2852a | |||
0fbda9441a | |||
32a82bbb7c | |||
628d0bb690 | |||
05223c1a5f | |||
9aa0603d87 | |||
36f980135f | |||
63953e42a8 | |||
4ba836f1ff | |||
b7132ab66a | |||
67810d50cb | |||
b7503c994c | |||
389695751f | |||
dad3039c06 |
@ -28,7 +28,7 @@ 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.svg",
|
||||
CryptoImagePath = "imlegacy/liquid.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true,
|
||||
|
@ -23,7 +23,7 @@ namespace BTCPayServer
|
||||
"USDT_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
DisplayName = "Tether USD",
|
||||
DisplayName = "Liquid Tether",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "liquidnetwork",
|
||||
|
@ -22,5 +22,10 @@ namespace BTCPayServer
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public override string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
|
||||
{
|
||||
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,11 @@ namespace BTCPayServer
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual string GenerateBIP21(string cryptoInfoAddress, string cryptoInfoDue)
|
||||
{
|
||||
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue}";
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BTCPayNetworkBase
|
||||
|
@ -4,6 +4,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.1" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@ -10,21 +11,91 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Subject",
|
||||
table: "OpenIddictTokens",
|
||||
maxLength: 450,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldMaxLength: 450);
|
||||
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: "OpenIddictAuthorizations",
|
||||
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.AddColumn<string>(
|
||||
name: "Requirements",
|
||||
@ -34,27 +105,140 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Requirements",
|
||||
table: "OpenIddictApplications");
|
||||
if (!migrationBuilder.IsSqlite())
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ namespace BTCPayServer.Rating
|
||||
public enum RateSource
|
||||
{
|
||||
Coingecko,
|
||||
CoinAverage,
|
||||
Direct
|
||||
}
|
||||
public class AvailableRateProvider
|
||||
@ -11,11 +10,17 @@ 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, RateSource source)
|
||||
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)
|
||||
{
|
||||
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 exchange
|
||||
/// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider
|
||||
/// </summary>
|
||||
public class BackgroundFetcherRateProvider : IRateProvider
|
||||
{
|
||||
@ -139,6 +139,9 @@ 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
|
||||
@ -156,6 +159,9 @@ 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
|
||||
|
@ -1,53 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
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,6 +18,14 @@ 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)
|
||||
@ -31,12 +39,12 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public virtual Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates();
|
||||
return CoinGeckoRate ? GetCoinGeckoRates(cancellationToken) : GetCoinGeckoExchangeSpecificRates(1, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<ExchangeRates> GetCoinGeckoRates()
|
||||
private async Task<ExchangeRates> GetCoinGeckoRates(CancellationToken cancellationToken)
|
||||
{
|
||||
using var resp = await Client.GetAsync("exchange_rates");
|
||||
using var resp = await GetWithBackoffAsync("exchange_rates", cancellationToken);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
return new ExchangeRates(JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children()
|
||||
.Where(token => ((JProperty)token).Name != "btc")
|
||||
@ -45,9 +53,28 @@ namespace BTCPayServer.Services.Rates
|
||||
new BidAsk(((JProperty)token).Value["value"].Value<decimal>()))));
|
||||
}
|
||||
|
||||
private async Task<ExchangeRates> GetCoinGeckoExchangeSpecificRates(int page = 1)
|
||||
private async Task<HttpResponseMessage> GetWithBackoffAsync(string request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}");
|
||||
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);
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
List<ExchangeRate> result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers")
|
||||
@ -69,7 +96,7 @@ namespace BTCPayServer.Services.Rates
|
||||
var tasks = new List<Task<ExchangeRates>>();
|
||||
for (int i = 2; i <= totalPages; i++)
|
||||
{
|
||||
tasks.Add(GetCoinGeckoExchangeSpecificRates(i));
|
||||
tasks.Add(GetCoinGeckoExchangeSpecificRates(i, cancellationToken));
|
||||
}
|
||||
|
||||
foreach (var t in (await Task.WhenAll(tasks)))
|
||||
|
@ -49,18 +49,13 @@ namespace BTCPayServer.Services.Rates
|
||||
public ExchangeRates ExchangeRates { get; set; }
|
||||
public ExchangeException Exception { get; internal set; }
|
||||
}
|
||||
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
public RateProviderFactory(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_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
|
||||
{
|
||||
@ -76,19 +71,12 @@ 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
|
||||
@ -100,18 +88,17 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
|
||||
{
|
||||
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("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(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);
|
||||
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");
|
||||
}
|
||||
void InitExchanges()
|
||||
{
|
||||
@ -124,7 +111,6 @@ 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")));
|
||||
@ -138,51 +124,24 @@ 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]);
|
||||
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);
|
||||
}
|
||||
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 coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
{
|
||||
Exchange = supportedExchange.Id
|
||||
};
|
||||
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);
|
||||
var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko);
|
||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,91 +160,17 @@ 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["name"].ToString(),
|
||||
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["id"].ToString().ToLowerInvariant(), token["name"].ToString(),
|
||||
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko))
|
||||
.Concat(new[] { new AvailableRateProvider("gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
|
||||
.Concat(new[] { new AvailableRateProvider("gdax", "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">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
@ -7,7 +7,16 @@
|
||||
<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>
|
||||
|
@ -21,6 +21,6 @@ ENV SCREEN_HEIGHT 600 \
|
||||
SCREEN_WIDTH 1200
|
||||
|
||||
COPY . .
|
||||
RUN cd BTCPayServer.Tests && dotnet build /p:CI_TESTS=true
|
||||
RUN cd BTCPayServer.Tests && dotnet build /p:CI_TESTS=true /p:RazorCompileOnBuild=true
|
||||
WORKDIR /source/BTCPayServer.Tests
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
130
BTCPayServer.Tests/U2FTests.cs
Normal file
130
BTCPayServer.Tests/U2FTests.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.U2F;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using U2F.Core.Models;
|
||||
using U2F.Core.Utils;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class U2FTests
|
||||
{
|
||||
public const int TestTimeout = 60_000;
|
||||
|
||||
public U2FTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task U2ftest()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var accountController = tester.PayTester.GetController<AccountController>();
|
||||
var manageController = user.GetController<ManageController>();
|
||||
var mock = new MockU2FService(tester.PayTester.GetService<ApplicationDbContextFactory>());
|
||||
manageController._u2FService = mock;
|
||||
accountController._u2FService = mock;
|
||||
|
||||
Assert
|
||||
.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
Email = user.RegisterDetails.Email, Password = user.RegisterDetails.Password
|
||||
}));
|
||||
|
||||
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
|
||||
var addDeviceVM = Assert.IsType<AddU2FDeviceViewModel>(Assert
|
||||
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
|
||||
|
||||
Assert.NotEmpty(addDeviceVM.Challenge);
|
||||
Assert.Equal("testdevice", addDeviceVM.Name);
|
||||
Assert.NotEmpty(addDeviceVM.Version);
|
||||
Assert.Null(addDeviceVM.DeviceResponse);
|
||||
|
||||
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray(),
|
||||
Guid.NewGuid().ToByteArray(), 1);
|
||||
|
||||
mock.GetDevReg = () => devReg;
|
||||
mock.StartedAuthentication = () =>
|
||||
new StartedAuthentication("chocolate", addDeviceVM.AppId,
|
||||
devReg.KeyHandle.ByteArrayToBase64String());
|
||||
addDeviceVM.DeviceResponse = new RegisterResponse("ss",
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes("{typ:'x', challenge: 'fff'}"))).ToJson();
|
||||
Assert
|
||||
.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addDeviceVM));
|
||||
|
||||
Assert.Single(Assert.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
|
||||
var secondaryLoginViewModel = Assert.IsType<SecondaryLoginViewModel>(Assert
|
||||
.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
Email = user.RegisterDetails.Email, Password = user.RegisterDetails.Password
|
||||
})).Model);
|
||||
Assert.NotNull(secondaryLoginViewModel.LoginWithU2FViewModel);
|
||||
Assert.Single(secondaryLoginViewModel.LoginWithU2FViewModel.Challenges);
|
||||
Assert.Equal(secondaryLoginViewModel.LoginWithU2FViewModel.Challenge,
|
||||
secondaryLoginViewModel.LoginWithU2FViewModel.Challenges.First().challenge);
|
||||
|
||||
secondaryLoginViewModel.LoginWithU2FViewModel.DeviceResponse = new AuthenticateResponse(
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes(
|
||||
"{typ:'x', challenge: '" + secondaryLoginViewModel.LoginWithU2FViewModel.Challenge + "'}")),
|
||||
"dd", devReg.KeyHandle.ByteArrayToBase64String()).ToJson();
|
||||
Assert
|
||||
.IsType<RedirectToActionResult>(
|
||||
await accountController.LoginWithU2F(secondaryLoginViewModel.LoginWithU2FViewModel));
|
||||
}
|
||||
}
|
||||
|
||||
public class MockU2FService : U2FService
|
||||
{
|
||||
public Func<DeviceRegistration> GetDevReg;
|
||||
public Func<StartedAuthentication> StartedAuthentication;
|
||||
|
||||
public MockU2FService(ApplicationDbContextFactory contextFactory) : base(contextFactory)
|
||||
{
|
||||
}
|
||||
|
||||
protected override StartedRegistration StartDeviceRegistrationCore(string appId)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
|
||||
}
|
||||
|
||||
protected override DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration,
|
||||
RegisterResponse registerResponse)
|
||||
{
|
||||
return GetDevReg();
|
||||
}
|
||||
|
||||
protected override StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
|
||||
{
|
||||
return StartedAuthentication();
|
||||
}
|
||||
|
||||
protected override void FinishAuthenticationCore(StartedAuthentication authentication,
|
||||
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -862,6 +862,7 @@ 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"));
|
||||
@ -1188,13 +1189,13 @@ namespace BTCPayServer.Tests
|
||||
// Can generate API Key
|
||||
var repo = tester.PayTester.GetService<TokenRepository>();
|
||||
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
///////
|
||||
|
||||
// Generating a new one remove the previous
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey().GetAwaiter().GetResult());
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
|
||||
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.NotEqual(apiKey, apiKey2);
|
||||
////////
|
||||
@ -2683,16 +2684,12 @@ 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)
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider bf && !(bf.Inner is CoinGeckoRateProvider cg && !cg.CoinGeckoRate))
|
||||
.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();
|
||||
@ -2785,12 +2782,7 @@ noninventoryitem:
|
||||
|
||||
public static RateProviderFactory CreateBTCPayRateFactory()
|
||||
{
|
||||
return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
|
||||
}
|
||||
|
||||
private static MemoryCacheOptions CreateMemoryCache()
|
||||
{
|
||||
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
|
||||
return new RateProviderFactory(new MockHttpClientFactory());
|
||||
}
|
||||
|
||||
class SpyRateProvider : IRateProvider
|
||||
@ -2894,42 +2886,17 @@ noninventoryitem:
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
var spy = new SpyRateProvider();
|
||||
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||
RateRules.TryParse("X_X = bittrex(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);
|
||||
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
|
||||
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();
|
||||
|
@ -10,13 +10,34 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class Utils
|
||||
{
|
||||
public static int _nextPort = 8001;
|
||||
public static object _portLock = new object();
|
||||
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
lock (_portLock)
|
||||
{
|
||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var port = _nextPort++;
|
||||
socket.Bind(new IPEndPoint(IPAddress.Loopback, port));
|
||||
return port;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// Retry unless exhausted
|
||||
if (_nextPort == 65536)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/a/14933880/2061103
|
||||
|
@ -64,7 +64,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
image: btcpayserver/bitcoin:0.19.0.1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -108,7 +108,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
image: btcpayserver/bitcoin:0.19.0.1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
@ -257,7 +257,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.7.1-beta-withseed
|
||||
image: btcpayserver/lnd:v0.8.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -287,7 +287,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.7.1-beta-withseed
|
||||
image: btcpayserver/lnd:v0.8.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -1,6 +1,9 @@
|
||||
<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>
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.U2F.Models;
|
||||
using Newtonsoft.Json;
|
||||
using NicolasDorier.RateLimits;
|
||||
using BTCPayServer.Data;
|
||||
using U2F.Core.Exceptions;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -38,7 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly U2FService _u2FService;
|
||||
public U2FService _u2FService;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -235,9 +236,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (U2fException e)
|
||||
{
|
||||
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers
|
||||
return new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
|
||||
PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId),
|
||||
IsLightning =
|
||||
kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using U2F.Core.Exceptions;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -65,7 +66,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction("U2FAuthentication");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (U2fException e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
IWebHostEnvironment _Env;
|
||||
private readonly U2FService _u2FService;
|
||||
public U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
@ -49,17 +49,27 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
DataWrapper<InvoiceResponse> invoice = null;
|
||||
try
|
||||
{
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CheckoutQueryString))
|
||||
{
|
||||
return Redirect(invoice.Data.Url);
|
||||
|
@ -84,76 +84,6 @@ 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)
|
||||
{
|
||||
|
@ -774,14 +774,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey()
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -48,9 +48,9 @@ namespace BTCPayServer
|
||||
throw new FormatException();
|
||||
}
|
||||
if (type == DerivationType.Segwit)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
|
||||
return new DirectDerivationStrategy(extPubKey, true);
|
||||
if (type == DerivationType.Legacy)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
|
||||
return new DirectDerivationStrategy(extPubKey, false);
|
||||
if (type == DerivationType.SegwitP2SH)
|
||||
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
|
||||
throw new FormatException();
|
||||
@ -79,7 +79,10 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
{
|
||||
hintedLabels.Add("legacy");
|
||||
str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -29,14 +29,11 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
RateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
RateProviderFactory rateProviderFactory,
|
||||
CoinAverageSettings coinAverageSettings)
|
||||
RateProviderFactory rateProviderFactory)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
}
|
||||
|
||||
@ -44,7 +41,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
return new Task[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSettings),
|
||||
CreateLoopTask(RefreshRates)
|
||||
};
|
||||
}
|
||||
@ -142,20 +138,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<U2FService>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -275,7 +274,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.AddSerilog(Serilog.Log.Logger);
|
||||
logBuilder.AddProvider(new Serilog.Extensions.Logging.SerilogLoggerProvider(Log.Logger));
|
||||
}
|
||||
});
|
||||
return services;
|
||||
|
@ -1,20 +0,0 @@
|
||||
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, GetName(a), a.Url, a.Source)).ToArray();
|
||||
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.SourceId, 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,9 +33,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
case Rating.RateSource.Direct:
|
||||
return a.Name;
|
||||
case Rating.RateSource.Coingecko:
|
||||
return $"{a.Name} (via CoinGecko, free)";
|
||||
case Rating.RateSource.CoinAverage:
|
||||
return $"{a.Name} (via BitcoinAverage, commercial)";
|
||||
return $"{a.Name} (via CoinGecko)";
|
||||
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=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
|
||||
"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=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
|
||||
"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",
|
||||
|
@ -461,8 +461,6 @@ 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
|
||||
@ -470,7 +468,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
BIP21 = ((BTCPayNetwork)info.Network).GenerateBIP21(cryptoInfo.Address, cryptoInfo.Due),
|
||||
};
|
||||
|
||||
#pragma warning disable 618
|
||||
|
@ -85,7 +85,15 @@ namespace BTCPayServer.Services
|
||||
catch (DbUpdateException) // Does not exists
|
||||
{
|
||||
entity.State = EntityState.Added;
|
||||
await ctx.SaveChangesAsync();
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
catch(DbUpdateException) // the Wallet does not exists in the DB
|
||||
{
|
||||
await SetWalletInfo(walletId, new WalletBlobInfo());
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using U2F.Core.Models;
|
||||
using U2F.Core.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using U2F.Core.Exceptions;
|
||||
|
||||
namespace BTCPayServer.U2F
|
||||
{
|
||||
@ -66,7 +67,7 @@ namespace BTCPayServer.U2F
|
||||
|
||||
public ServerRegisterResponse StartDeviceRegistration(string userId, string appId)
|
||||
{
|
||||
var startedRegistration = global::U2F.Core.Crypto.U2F.StartRegistration(appId);
|
||||
var startedRegistration = StartDeviceRegistrationCore(appId);
|
||||
|
||||
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>()
|
||||
{
|
||||
@ -103,7 +104,7 @@ namespace BTCPayServer.U2F
|
||||
|
||||
var startedRegistration =
|
||||
new StartedRegistration(authenticationRequest.Challenge, authenticationRequest.AppId);
|
||||
var registration = global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse);
|
||||
var registration = FinishRegistrationCore(startedRegistration, registerResponse);
|
||||
|
||||
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>());
|
||||
using (var context = _contextFactory.CreateContext())
|
||||
@ -115,7 +116,7 @@ namespace BTCPayServer.U2F
|
||||
|
||||
if (duplicate)
|
||||
{
|
||||
throw new InvalidOperationException("The U2F Device has already been registered with this user");
|
||||
throw new U2fException("The U2F Device has already been registered with this user");
|
||||
}
|
||||
|
||||
await context.U2FDevices.AddAsync(new U2FDevice()
|
||||
@ -173,7 +174,7 @@ namespace BTCPayServer.U2F
|
||||
{
|
||||
authentication = new StartedAuthentication(challengeAuthenticationRequestMatch.Challenge, authenticationRequest.AppId, authenticationRequest.KeyHandle);
|
||||
}
|
||||
global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration);
|
||||
FinishAuthenticationCore(authentication, authenticateResponse, registration);
|
||||
|
||||
UserAuthenticationRequests.AddOrReplace(userId, new List<U2FDeviceAuthenticationRequest>());
|
||||
|
||||
@ -200,9 +201,7 @@ namespace BTCPayServer.U2F
|
||||
var serverChallenges = new List<ServerChallenge>();
|
||||
foreach (var registeredDevice in devices)
|
||||
{
|
||||
var challenge = global::U2F.Core.Crypto.U2F.StartAuthentication(appId,
|
||||
new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey,
|
||||
registeredDevice.AttestationCert, (uint)registeredDevice.Counter));
|
||||
var challenge = StartAuthenticationCore(appId, registeredDevice);
|
||||
serverChallenges.Add(new ServerChallenge()
|
||||
{
|
||||
challenge = challenge.Challenge,
|
||||
@ -225,5 +224,28 @@ namespace BTCPayServer.U2F
|
||||
return serverChallenges;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual StartedRegistration StartDeviceRegistrationCore(string appId)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
|
||||
}
|
||||
|
||||
protected virtual DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration, RegisterResponse registerResponse)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse);
|
||||
}
|
||||
|
||||
protected virtual StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
|
||||
{
|
||||
return global::U2F.Core.Crypto.U2F.StartAuthentication(appId,
|
||||
new DeviceRegistration(registeredDevice.KeyHandle, registeredDevice.PublicKey,
|
||||
registeredDevice.AttestationCert, (uint)registeredDevice.Counter));
|
||||
}
|
||||
|
||||
protected virtual void FinishAuthenticationCore(StartedAuthentication authentication,
|
||||
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
|
||||
{
|
||||
global::U2F.Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,10 @@
|
||||
|
||||
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default(long))
|
||||
{
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod" class="mt-4" enctype="multipart/form-data">
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
|
||||
class="mt-4" enctype="multipart/form-data">
|
||||
|
||||
<div class="card my-2">
|
||||
<h3 class="card-title p-2">Upload Wallet</h3>
|
||||
@ -58,7 +61,10 @@
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod" class="mt-4" enctype="multipart/form-data">
|
||||
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
|
||||
class="mt-4" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" asp-for="CryptoCode"/>
|
||||
@if (!Model.WalletFileFound || Model.Summary.WalletHeight == default(long))
|
||||
@ -100,7 +106,13 @@
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
|
||||
|
||||
<a class="btn btn-secondary" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="MoneroLikeStore">Back to list</a>
|
||||
<a class="btn btn-secondary" asp-action="GetStoreMoneroLikePaymentMethods"
|
||||
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
|
||||
asp-controller="MoneroLikeStore">
|
||||
Back to list
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -3,14 +3,14 @@
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
|
||||
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.ActivePage, "Monero Settings");
|
||||
|
||||
|
||||
ViewData["NavPartialName"] = "../Stores/_Nav";
|
||||
}
|
||||
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -19,40 +19,44 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Account Index</th>
|
||||
<th class="text-center">Enabled</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.CryptoCode</td>
|
||||
<td>@item.AccountIndex</td>
|
||||
<td class="text-center">
|
||||
@if(item.Enabled)
|
||||
{
|
||||
<span class="fa fa-check"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a id="Modify" asp-action="GetStoreMoneroLikePaymentMethod" asp-route-cryptoCode="@item.CryptoCode">Modify</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Account Index</th>
|
||||
<th class="text-center">Enabled</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.CryptoCode</td>
|
||||
<td>@item.AccountIndex</td>
|
||||
<td class="text-center">
|
||||
@if (item.Enabled)
|
||||
{
|
||||
<span class="fa fa-check"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a id="Modify" asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@item.CryptoCode">
|
||||
Modify
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
@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, Rates, Emails, Policies, Theme, Services, Maintenance, Logs, Files
|
||||
Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<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>
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Services.Altcoins.Monero.Configuration
|
||||
@using BTCPayServer.Services.Altcoins.Monero.UI
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@ -9,5 +8,5 @@
|
||||
}
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||
{
|
||||
<a class="nav-link @(isMonero ? "active" : string.Empty)" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="MoneroLikeStore">Monero</a>
|
||||
<a class="nav-link @(isMonero ? "active" : string.Empty)" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="MoneroLikeStore">Monero</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 you the certificate is trusted by your machine</p>
|
||||
<p>You can omit <code>certthumbprint</code> if 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">
|
||||
|
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<a id="CreateNewToken" asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
|
||||
<a id="CreateNewToken" asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus" asp-route-storeId="@this.Context.GetRouteValue("storeId")"></span> Create a new token</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -34,7 +34,7 @@
|
||||
<tr>
|
||||
<td>@token.Label</td>
|
||||
<td class="text-right">
|
||||
<a asp-action="ShowToken" asp-route-tokenId="@token.Id">See information</a> - <a asp-action="RevokeToken" asp-route-tokenId="@token.Id">Revoke</a>
|
||||
<a asp-action="ShowToken" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id">See information</a> - <a asp-action="RevokeToken" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id">Revoke</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -52,7 +52,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post" asp-action="GenerateAPIKey">
|
||||
<form method="post" asp-action="GenerateAPIKey" asp-route-storeId="@this.Context.GetRouteValue("storeId")">
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiKey"></label>
|
||||
<input asp-for="ApiKey" readonly class="form-control" />
|
||||
|
@ -55,23 +55,6 @@
|
||||
</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>
|
||||
}
|
||||
|
@ -203,7 +203,7 @@
|
||||
<span class="fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right"><a asp-action="@scheme.Action" id='Modify-@scheme.Provider'>Modify</a></td>
|
||||
<td class="text-right"><a asp-action="@scheme.Action" id='Modify-@scheme.Provider' asp-route-storeId="@this.Context.GetRouteValue("storeId")">Modify</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@ -227,7 +227,7 @@
|
||||
<td>
|
||||
Email
|
||||
</td>
|
||||
<td style="text-align:right"><a asp-action="Emails" >Modify</a></td>
|
||||
<td style="text-align:right"><a asp-action="Emails" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Modify</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -103,7 +103,8 @@
|
||||
>
|
||||
@label.Value
|
||||
<form
|
||||
asp-action="ModifyTransaction"
|
||||
asp-route-walletId="@this.Context.GetRouteValue("walletId")"
|
||||
asp-action="ModifyTransaction"
|
||||
method="post"
|
||||
class="removeTransactionLabelForm"
|
||||
>
|
||||
@ -136,7 +137,8 @@
|
||||
<div class="dropdown" style="display:inline-block;">
|
||||
<span class="fa fa-tags" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu">
|
||||
<form asp-action="ModifyTransaction" method="post">
|
||||
<form asp-action="ModifyTransaction" method="post"
|
||||
asp-route-walletId="@this.Context.GetRouteValue("walletId")">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<div class="dropdown-item input-group">
|
||||
<input name="addlabel" placeholder="Label name" maxlength="20" type="text" class="form-control form-control-sm" />
|
||||
@ -169,7 +171,8 @@
|
||||
<span class="fa fa-commenting" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
}
|
||||
<div class="dropdown-menu">
|
||||
<form asp-action="ModifyTransaction" method="post">
|
||||
<form asp-action="ModifyTransaction" method="post"
|
||||
asp-route-walletId="@this.Context.GetRouteValue("walletId")">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<textarea name="addcomment" class="dropdown-item" maxlength="200" rows="2" cols="20">@transaction.Comment</textarea>
|
||||
<div class="dropdown-item"><button type="submit" class="btn btn-primary">Save comment</button></div>
|
||||
|
@ -1 +1 @@
|
||||
<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>
|
||||
<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>
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 874 B |
BIN
BTCPayServer/wwwroot/imlegacy/liquid.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/liquid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -1,34 +0,0 @@
|
||||
<?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>
|
Before Width: | Height: | Size: 5.5 KiB |
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.0.3.148</Version>
|
||||
<Version>1.0.3.151</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{29290EC7-0
|
||||
Build\Common.csproj = Build\Common.csproj
|
||||
.circleci\config.yml = .circleci\config.yml
|
||||
docker-entrypoint.sh = docker-entrypoint.sh
|
||||
.circleci\run-tests.sh = .circleci\run-tests.sh
|
||||
Build\Version.csproj = Build\Version.csproj
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
Reference in New Issue
Block a user