Compare commits
127 Commits
v1.0.3.149
...
v1.0.3.159
Author | SHA1 | Date | |
---|---|---|---|
ee524e36c5 | |||
f097ecdc80 | |||
a354f7d9dd | |||
29d51ad2a2 | |||
1be6408246 | |||
34702d2633 | |||
b79b310bd5 | |||
dc4f8a1fbe | |||
6d0896084f | |||
d31bff7070 | |||
f2aab4cf03 | |||
c03dc48fe9 | |||
143c909812 | |||
821b904163 | |||
6015eb337a | |||
5d817a0483 | |||
ee9905e85a | |||
ff4c7c364e | |||
a2d657f5cb | |||
db6a4687d2 | |||
07f0d95f56 | |||
1a409a441d | |||
445e184154 | |||
9a10f55a85 | |||
ae33b1d0a8 | |||
4ed2db83a5 | |||
500aa85142 | |||
3b6cc84a93 | |||
5ce29d2bb8 | |||
3184d2b2df | |||
f5e65ec2a6 | |||
66488d813b | |||
4853cfe41a | |||
dc7733abcd | |||
771c8e2758 | |||
24664b60af | |||
82393eb8bb | |||
b432d8903f | |||
ea9169f607 | |||
496a6f0f55 | |||
fb2a0fb7fb | |||
ef503fa907 | |||
fe2eca4fda | |||
88835b5b55 | |||
876c940032 | |||
a08d5be35c | |||
0074790684 | |||
23aaf794ef | |||
bb12d37416 | |||
e058903450 | |||
06f1c17a5f | |||
e00136de93 | |||
56d8c033d7 | |||
666682677c | |||
652b958d4f | |||
c7c0db612a | |||
a83edce4dc | |||
f99058a9fa | |||
a907143d81 | |||
4ae173bb69 | |||
1436420a93 | |||
086cbaa231 | |||
5dd3112e0d | |||
b42e4f240a | |||
7076692069 | |||
dcb3601791 | |||
54c7c0d696 | |||
f324185d82 | |||
a63502873c | |||
f5cbf6672a | |||
a78dff5931 | |||
f8139a9156 | |||
27a61b7afd | |||
71671b9e16 | |||
c68bf5220e | |||
80ee03d897 | |||
d0bfa67495 | |||
bdb2edba12 | |||
78d8f4e011 | |||
1bfe9dda97 | |||
8e6f43cd3a | |||
6848482999 | |||
43967ee86e | |||
61b99f6630 | |||
7e073fb7e1 | |||
1ceb5cb576 | |||
53a60d1660 | |||
25b733ca7f | |||
1bf680fdb9 | |||
76008c9f5c | |||
1bf04ac4ac | |||
4b088defd3 | |||
a2be7ee471 | |||
025da0261d | |||
4ac79a7ea3 | |||
ef20a03b95 | |||
d9681398e5 | |||
25f19e5d9f | |||
aab6fcd508 | |||
a8ac01cd8b | |||
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -293,3 +293,4 @@ BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
||||
.vscode
|
||||
BTCPayServer/testpwd
|
||||
|
@ -16,13 +16,13 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Polis",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "polis",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"POLIS_X = POLIS_BTC * BTC_X",
|
||||
"POLIS_BTC = cryptopia(POLIS_BTC)"
|
||||
"POLIS_BTC = polispay(POLIS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/polis.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -59,7 +59,8 @@ namespace BTCPayServer
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
|
||||
InitPolis();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||
{
|
||||
@ -77,7 +78,6 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
//InitUfo();
|
||||
}
|
||||
|
@ -28,24 +28,10 @@ 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,
|
||||
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy }, // xpub
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
|
||||
{0x4b24746U, DerivationType.Segwit }, //zpub
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy},
|
||||
{0x044a5262U, DerivationType.SegwitP2SH},
|
||||
{0x045f1cf6U, DerivationType.Segwit}
|
||||
},
|
||||
SupportRBF = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -16,6 +11,7 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = "USDt",
|
||||
NetworkCryptoCode = "LBTC",
|
||||
ShowSyncSummary = false,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"USDT_UST = 1",
|
||||
@ -23,28 +19,37 @@ 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",
|
||||
CryptoImagePath = "imlegacy/liquid-tether.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true,
|
||||
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy }, // xpub
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
|
||||
{0x4b24746U, DerivationType.Segwit }, //zpub
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy},
|
||||
{0x044a5262U, DerivationType.SegwitP2SH},
|
||||
{0x045f1cf6U, DerivationType.Segwit}
|
||||
}
|
||||
SupportRBF = true
|
||||
});
|
||||
|
||||
Add(new ElementsBTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "ETB",
|
||||
NetworkCryptoCode = "LBTC",
|
||||
ShowSyncSummary = false,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
|
||||
"ETB_X = ETB_BTC * BTC_X",
|
||||
"ETB_BTC = bitpay(ETB_BTC)"
|
||||
},
|
||||
Divisibility = 2,
|
||||
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
DisplayName = "Ethiopian Birr",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "liquidnetwork",
|
||||
CryptoImagePath = "imlegacy/etb.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -6,21 +6,26 @@ using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ElementsBTCPayNetwork:BTCPayNetwork
|
||||
public class ElementsBTCPayNetwork : BTCPayNetwork
|
||||
{
|
||||
public string NetworkCryptoCode { get; set; }
|
||||
public uint256 AssetId { get; set; }
|
||||
public override bool ReadonlyWallet { get; set; } = true;
|
||||
public int Divisibility { get; set; } = 8;
|
||||
|
||||
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs)
|
||||
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(
|
||||
NewTransactionEvent evtOutputs)
|
||||
{
|
||||
return evtOutputs.Outputs.Where(output =>
|
||||
return evtOutputs.Outputs.Where(output =>
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
|
||||
{
|
||||
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
{
|
||||
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = "XMR",
|
||||
DisplayName = "Monero",
|
||||
Divisibility = 12,
|
||||
BlockExplorerLink =
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://www.exploremonero.com/transaction/{0}"
|
||||
|
@ -112,14 +112,20 @@ namespace BTCPayServer
|
||||
return (output, outpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
{
|
||||
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BTCPayNetworkBase
|
||||
{
|
||||
public bool ShowSyncSummary { get; set; } = true;
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public int Divisibility { get; set; } = 8;
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -3,10 +3,11 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.20058.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -2,33 +2,33 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using OpenIddict.EntityFrameworkCore.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
|
||||
{
|
||||
public ApplicationDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
|
||||
builder.UseSqlite("Data Source=temp.db");
|
||||
|
||||
return new ApplicationDbContext(builder.Options, true);
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
//public ApplicationDbContext(): base(CreateMySql())
|
||||
//{
|
||||
private readonly bool _designTime;
|
||||
|
||||
//}
|
||||
|
||||
//private static DbContextOptions CreateMySql()
|
||||
//{
|
||||
// return new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
// .UseMySql("Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;")
|
||||
// .Options;
|
||||
//}
|
||||
|
||||
public ApplicationDbContext()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, bool designTime = false)
|
||||
: base(options)
|
||||
{
|
||||
_designTime = designTime;
|
||||
}
|
||||
|
||||
public DbSet<InvoiceData> Invoices
|
||||
@ -257,6 +257,26 @@ namespace BTCPayServer.Data
|
||||
|
||||
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
|
||||
|
||||
if (Database.IsSqlite() && !_designTime)
|
||||
{
|
||||
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
|
||||
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
|
||||
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
|
||||
// use the DateTimeOffsetToBinaryConverter
|
||||
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
|
||||
// This only supports millisecond precision, but should be sufficient for most use cases.
|
||||
foreach (var entityType in builder.Model.GetEntityTypes())
|
||||
{
|
||||
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
|
||||
foreach (var property in properties)
|
||||
{
|
||||
builder
|
||||
.Entity(entityType.Name)
|
||||
.Property(property.Name)
|
||||
.HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,31 +14,16 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.14-servicing-32113");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
.HasAnnotation("ProductVersion", "3.1.1");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -48,22 +33,46 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppType");
|
||||
b.Property<string>("AppType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name");
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
b.Property<string>("Settings")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TagAllInvoices");
|
||||
b.Property<bool>("TagAllInvoices")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -75,41 +84,56 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
b.Property<bool>("RequiresEmailConfirmation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -127,27 +151,35 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationId");
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Properties");
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scopes");
|
||||
b.Property<string>("Scopes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -160,36 +192,49 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("ClientSecret");
|
||||
b.Property<string>("ClientSecret")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("ConsentType");
|
||||
b.Property<string>("ConsentType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName");
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions");
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PostLogoutRedirectUris");
|
||||
b.Property<string>("PostLogoutRedirectUris")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties");
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RedirectUris");
|
||||
b.Property<string>("RedirectUris")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Requirements");
|
||||
b.Property<string>("Requirements")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -205,36 +250,48 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationId");
|
||||
b.Property<string>("ApplicationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthorizationId");
|
||||
b.Property<string>("AuthorizationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<DateTimeOffset?>("CreationDate");
|
||||
b.Property<DateTimeOffset?>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpirationDate");
|
||||
b.Property<DateTimeOffset?>("ExpirationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Payload");
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties");
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(100);
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(450);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(25);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -251,15 +308,20 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address");
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
b.Property<DateTimeOffset>("Assigned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
b.Property<string>("CryptoCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
b.Property<DateTimeOffset?>("UnAssigned")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
@ -269,23 +331,31 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
b.Property<string>("CustomerEmail")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
b.Property<string>("ExceptionStatus")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
b.Property<string>("ItemCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
b.Property<string>("OrderId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status");
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -296,13 +366,17 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
b.Property<string>("UniqueId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message");
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
@ -312,15 +386,19 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Label");
|
||||
b.Property<string>("Label")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
b.Property<DateTimeOffset>("PairingTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
b.Property<string>("SIN")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -334,21 +412,28 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
b.Property<DateTimeOffset>("Expiration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
b.Property<string>("Facade")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Label");
|
||||
b.Property<string>("Label")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
b.Property<string>("SIN")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
b.Property<string>("TokenValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -358,13 +443,16 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
b.Property<bool>("Accounted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -376,17 +464,21 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
b.Property<int>("Status");
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -399,7 +491,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id");
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -409,11 +502,13 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -425,9 +520,10 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value");
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -437,23 +533,31 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
b.Property<string>("DefaultCrypto")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
b.Property<string>("DerivationStrategies")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
b.Property<string>("DerivationStrategy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
b.Property<int>("SpeedPolicy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
b.Property<byte[]>("StoreBlob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
b.Property<byte[]>("StoreCertificate")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
b.Property<string>("StoreName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
b.Property<string>("StoreWebsite")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -463,15 +567,20 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName");
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageFileName");
|
||||
b.Property<string>("StorageFileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp");
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -483,22 +592,28 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId");
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("AttestationCert")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<int>("Counter");
|
||||
b.Property<int>("Counter")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("KeyHandle")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name");
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("PublicKey")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -509,11 +624,14 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role");
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
@ -525,9 +643,10 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -536,13 +655,17 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId");
|
||||
b.Property<string>("WalletDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TransactionId");
|
||||
b.Property<string>("TransactionId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Labels");
|
||||
b.Property<string>("Labels")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletDataId", "TransactionId");
|
||||
|
||||
@ -552,15 +675,18 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -575,14 +701,18 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -594,14 +724,18 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -612,14 +746,18 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
@ -630,9 +768,11 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
@ -643,13 +783,17 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name");
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value");
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
@ -659,23 +803,30 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyToken")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("Description");
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName");
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(200);
|
||||
|
||||
b.Property<string>("Properties");
|
||||
b.Property<string>("Properties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Resources");
|
||||
b.Property<string>("Resources")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -685,14 +836,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("OpenIddictScopes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -701,6 +844,14 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -739,7 +890,8 @@ namespace BTCPayServer.Migrations
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
@ -755,7 +907,8 @@ namespace BTCPayServer.Migrations
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
@ -787,7 +940,8 @@ namespace BTCPayServer.Migrations
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
@ -817,12 +971,14 @@ namespace BTCPayServer.Migrations
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
@ -830,52 +986,59 @@ namespace BTCPayServer.Migrations
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
.WithMany("WalletTransactions")
|
||||
.HasForeignKey("WalletDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser")
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser")
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser")
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser")
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -2,10 +2,6 @@
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Providers\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
|
||||
|
@ -11,7 +11,15 @@ namespace BTCPayServer.Rating
|
||||
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
|
||||
public ExchangeRates()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ExchangeRates(string exchangeName, IEnumerable<PairRate> rates)
|
||||
{
|
||||
foreach (var rate in rates)
|
||||
{
|
||||
Add(new ExchangeRate(exchangeName, rate.CurrencyPair, rate.BidAsk));
|
||||
}
|
||||
}
|
||||
public ExchangeRates(IEnumerable<ExchangeRate> rates)
|
||||
{
|
||||
@ -218,6 +226,26 @@ namespace BTCPayServer.Rating
|
||||
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
}
|
||||
|
||||
public class PairRate
|
||||
{
|
||||
public PairRate(CurrencyPair currencyPair, BidAsk bidAsk)
|
||||
{
|
||||
if (currencyPair == null)
|
||||
throw new ArgumentNullException(nameof(currencyPair));
|
||||
if (bidAsk == null)
|
||||
throw new ArgumentNullException(nameof(bidAsk));
|
||||
this.CurrencyPair = currencyPair;
|
||||
this.BidAsk = bidAsk;
|
||||
}
|
||||
public CurrencyPair CurrencyPair { get; }
|
||||
public BidAsk BidAsk { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{CurrencyPair} == {BidAsk}";
|
||||
}
|
||||
}
|
||||
public class ExchangeRate
|
||||
{
|
||||
public ExchangeRate()
|
||||
|
@ -54,20 +54,19 @@ 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
|
||||
{
|
||||
public class LatestFetch
|
||||
{
|
||||
public ExchangeRates Latest;
|
||||
public PairRate[] Latest;
|
||||
public DateTimeOffset NextRefresh;
|
||||
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
|
||||
public DateTimeOffset Updated;
|
||||
public DateTimeOffset Expiration;
|
||||
public Exception Exception;
|
||||
public string ExchangeName;
|
||||
internal ExchangeRates GetResult()
|
||||
internal PairRate[] GetResult()
|
||||
{
|
||||
if (Expiration <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
@ -77,7 +76,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"The rate has expired ({ExchangeName})");
|
||||
throw new InvalidOperationException($"The rate has expired");
|
||||
}
|
||||
}
|
||||
return Latest;
|
||||
@ -87,28 +86,23 @@ namespace BTCPayServer.Services.Rates
|
||||
IRateProvider _Inner;
|
||||
public IRateProvider Inner => _Inner;
|
||||
|
||||
public BackgroundFetcherRateProvider(string exchangeName, IRateProvider inner)
|
||||
public BackgroundFetcherRateProvider(IRateProvider inner)
|
||||
{
|
||||
if (inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
if (exchangeName == null)
|
||||
throw new ArgumentNullException(nameof(exchangeName));
|
||||
_Inner = inner;
|
||||
ExchangeName = exchangeName;
|
||||
}
|
||||
|
||||
public BackgroundFetcherState GetState()
|
||||
{
|
||||
var state = new BackgroundFetcherState()
|
||||
{
|
||||
ExchangeName = ExchangeName,
|
||||
LastRequested = LastRequested
|
||||
};
|
||||
if (_Latest is LatestFetch fetch)
|
||||
{
|
||||
state.LastUpdated = fetch.Updated;
|
||||
state.Rates = fetch.Latest
|
||||
.Where(e => e.Exchange == ExchangeName)
|
||||
.Select(r => new BackgroundFetcherRate()
|
||||
{
|
||||
Pair = r.CurrencyPair,
|
||||
@ -120,16 +114,13 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public void LoadState(BackgroundFetcherState state)
|
||||
{
|
||||
if (ExchangeName != state.ExchangeName)
|
||||
throw new InvalidOperationException("The state does not belong to this fetcher");
|
||||
if (state.LastRequested is DateTimeOffset lastRequested)
|
||||
this.LastRequested = state.LastRequested;
|
||||
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
|
||||
{
|
||||
var fetch = new LatestFetch()
|
||||
{
|
||||
ExchangeName = state.ExchangeName,
|
||||
Latest = new ExchangeRates(rates.Select(r => new ExchangeRate(state.ExchangeName, r.Pair, r.BidAsk))),
|
||||
Latest = rates.Select(r => new PairRate(r.Pair, r.BidAsk)).ToArray(),
|
||||
Updated = updated,
|
||||
NextRefresh = updated + RefreshRate,
|
||||
Expiration = updated + ValidatyTime
|
||||
@ -139,6 +130,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 +150,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
|
||||
@ -201,7 +198,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
LatestFetch _Latest;
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequested = DateTimeOffset.UtcNow;
|
||||
var latest = _Latest;
|
||||
@ -217,7 +214,6 @@ namespace BTCPayServer.Services.Rates
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastRequested { get; set; }
|
||||
|
||||
public string ExchangeName { get; }
|
||||
public DateTimeOffset? Expiration
|
||||
{
|
||||
get
|
||||
@ -235,7 +231,6 @@ namespace BTCPayServer.Services.Rates
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var previous = _Latest;
|
||||
var fetch = new LatestFetch();
|
||||
fetch.ExchangeName = ExchangeName;
|
||||
try
|
||||
{
|
||||
var rates = await _Inner.GetRatesAsync(cancellationToken);
|
||||
|
@ -9,24 +9,22 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitbankRateProvider : IRateProvider, IHasExchangeName
|
||||
public class BitbankRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitbankRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bitbank";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
|
||||
return ((jobj["data"] as JObject) ?? new JObject())
|
||||
.Properties()
|
||||
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
|
||||
.ToArray());
|
||||
|
||||
.Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static BidAsk CreateBidAsk(JProperty p)
|
||||
|
@ -10,26 +10,23 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class BitpayRateProvider : IRateProvider, IHasExchangeName
|
||||
public class BitpayRateProvider : IRateProvider
|
||||
{
|
||||
public const string BitpayName = "bitpay";
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitpayRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public string ExchangeName => BitpayName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken);
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
|
||||
return new ExchangeRates(jarray
|
||||
return jarray
|
||||
.Children<JObject>()
|
||||
.Select(jobj => new ExchangeRate(ExchangeName, new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>())))
|
||||
.Select(jobj => new PairRate(new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>())))
|
||||
.Where(o => o.CurrencyPair.Right != "BTC")
|
||||
.ToArray());
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,21 +7,20 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ByllsRateProvider : IRateProvider, IHasExchangeName
|
||||
public class ByllsRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public ByllsRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public string ExchangeName => "bylls";
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["public_price"]["to_price"].Value<decimal>();
|
||||
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
|
||||
return new[] { new PairRate(new CurrencyPair("BTC", "CAD"), new BidAsk(value)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
@ -11,18 +12,15 @@ using ExchangeSharp;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName
|
||||
public class ExchangeSharpRateProvider<T> : IRateProvider where T : ExchangeAPI, new()
|
||||
{
|
||||
readonly ExchangeAPI _ExchangeAPI;
|
||||
readonly string _ExchangeName;
|
||||
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
|
||||
HttpClient _httpClient;
|
||||
public ExchangeSharpRateProvider(HttpClient httpClient, bool reverseCurrencyPair = false)
|
||||
{
|
||||
if (exchangeAPI == null)
|
||||
throw new ArgumentNullException(nameof(exchangeAPI));
|
||||
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
|
||||
_ExchangeAPI = exchangeAPI;
|
||||
_ExchangeName = exchangeName;
|
||||
if (httpClient == null)
|
||||
throw new ArgumentNullException(nameof(httpClient));
|
||||
ReverseCurrencyPair = reverseCurrencyPair;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public bool ReverseCurrencyPair
|
||||
@ -30,45 +28,42 @@ namespace BTCPayServer.Services.Rates
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ExchangeName => _ExchangeName;
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = await _ExchangeAPI.GetTickersAsync();
|
||||
|
||||
var exchangeRateTasks = rates
|
||||
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
|
||||
.Select(t => CreateExchangeRate(t));
|
||||
var exchangeAPI = new T();
|
||||
exchangeAPI.RequestMaker = new HttpClientRequestMaker(exchangeAPI, _httpClient, cancellationToken);
|
||||
var rates = await exchangeAPI.GetTickersAsync();
|
||||
|
||||
var exchangeRates = await Task.WhenAll(exchangeRateTasks);
|
||||
|
||||
return new ExchangeRates(exchangeRates
|
||||
var exchangeRateTasks = rates
|
||||
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
|
||||
.Select(t => CreateExchangeRate(exchangeAPI, t));
|
||||
|
||||
var exchangeRates = await Task.WhenAll(exchangeRateTasks);
|
||||
|
||||
return exchangeRates
|
||||
.Where(t => t != null)
|
||||
.ToArray());
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
|
||||
private async Task<ExchangeRate> CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
|
||||
private async Task<PairRate> CreateExchangeRate(T exchangeAPI, KeyValuePair<string, ExchangeTicker> ticker)
|
||||
{
|
||||
if (notFoundSymbols.TryGetValue(ticker.Key, out _))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var tickerName = await _ExchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key);
|
||||
var tickerName = await exchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key);
|
||||
if (!CurrencyPair.TryParse(tickerName, out var pair))
|
||||
{
|
||||
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
|
||||
return null;
|
||||
}
|
||||
if(ReverseCurrencyPair)
|
||||
if (ReverseCurrencyPair)
|
||||
pair = new CurrencyPair(pair.Right, pair.Left);
|
||||
var rate = new ExchangeRate();
|
||||
rate.CurrencyPair = pair;
|
||||
rate.Exchange = _ExchangeName;
|
||||
rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask);
|
||||
return rate;
|
||||
return new PairRate(pair, new BidAsk(ticker.Value.Bid, ticker.Value.Ask));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
@ -17,7 +17,7 @@ namespace BTCPayServer.Services.Rates
|
||||
_Providers = providers;
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var p in _Providers)
|
||||
{
|
||||
@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
catch(Exception ex) { Exceptions.Add(ex); }
|
||||
}
|
||||
return new ExchangeRates();
|
||||
return Array.Empty<PairRate>();
|
||||
}
|
||||
|
||||
public List<Exception> Exceptions { get; set; } = new List<Exception>();
|
||||
|
216
BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs
Normal file
216
BTCPayServer.Rating/Providers/HttpClientRequestMaker.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ExchangeSharp;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
internal class HttpClientRequestMaker : IAPIRequestMaker
|
||||
{
|
||||
class InternalHttpWebRequest : IHttpWebRequest
|
||||
{
|
||||
internal readonly HttpWebRequest Request;
|
||||
|
||||
public Uri RequestUri => Request.RequestUri;
|
||||
|
||||
public string Method
|
||||
{
|
||||
get
|
||||
{
|
||||
return Request.Method;
|
||||
}
|
||||
set
|
||||
{
|
||||
Request.Method = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int Timeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return Request.Timeout;
|
||||
}
|
||||
set
|
||||
{
|
||||
Request.Timeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int ReadWriteTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return Request.ReadWriteTimeout;
|
||||
}
|
||||
set
|
||||
{
|
||||
Request.ReadWriteTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
public InternalHttpWebRequest(Uri fullUri)
|
||||
{
|
||||
Request = ((WebRequest.Create(fullUri) as HttpWebRequest) ?? throw new NullReferenceException("Failed to create HttpWebRequest"));
|
||||
Request.KeepAlive = false;
|
||||
}
|
||||
|
||||
public void AddHeader(string header, string value)
|
||||
{
|
||||
switch (header.ToStringLowerInvariant())
|
||||
{
|
||||
case "content-type":
|
||||
Request.ContentType = value;
|
||||
break;
|
||||
case "content-length":
|
||||
Request.ContentLength = value.ConvertInvariant<long>(0L);
|
||||
break;
|
||||
case "user-agent":
|
||||
Request.UserAgent = value;
|
||||
break;
|
||||
case "accept":
|
||||
Request.Accept = value;
|
||||
break;
|
||||
case "connection":
|
||||
Request.Connection = value;
|
||||
break;
|
||||
default:
|
||||
Request.Headers[header] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task WriteAllAsync(byte[] data, int index, int length)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public HttpRequestMessage ToHttpRequestMessage()
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get, Request.RequestUri);
|
||||
CopyHeadersFrom(httpRequest, Request);
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
internal void CopyHeadersFrom(HttpRequestMessage message, HttpWebRequest request)
|
||||
{
|
||||
foreach (string headerName in request.Headers)
|
||||
{
|
||||
string[] headerValues = request.Headers.GetValues(headerName);
|
||||
if (!message.Headers.TryAddWithoutValidation(headerName, headerValues))
|
||||
{
|
||||
if (message.Content != null)
|
||||
message.Content.Headers.TryAddWithoutValidation(headerName, headerValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class InternalHttpWebResponse : IHttpWebResponse
|
||||
{
|
||||
public InternalHttpWebResponse(HttpResponseMessage httpResponseMessage)
|
||||
{
|
||||
var headers = new Dictionary<string, List<string>>();
|
||||
foreach (var h in httpResponseMessage.Headers)
|
||||
{
|
||||
if (!headers.TryGetValue(h.Key, out var list))
|
||||
{
|
||||
list = new List<string>();
|
||||
headers.Add(h.Key, list);
|
||||
}
|
||||
list.AddRange(h.Value);
|
||||
}
|
||||
Headers = new Dictionary<string, IReadOnlyList<string>>(headers.Count);
|
||||
foreach (var item in headers)
|
||||
{
|
||||
Headers.Add(item.Key, item.Value.AsReadOnly());
|
||||
}
|
||||
}
|
||||
public Dictionary<string, IReadOnlyList<string>> Headers { get; }
|
||||
static IReadOnlyList<string> Empty = new List<string>().AsReadOnly();
|
||||
public IReadOnlyList<string> GetHeader(string name)
|
||||
{
|
||||
Headers.TryGetValue(name, out var list);
|
||||
return list ?? Empty;
|
||||
}
|
||||
}
|
||||
private readonly IAPIRequestHandler api;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
|
||||
public HttpClientRequestMaker(IAPIRequestHandler api, HttpClient httpClient, CancellationToken cancellationToken)
|
||||
{
|
||||
if (api == null)
|
||||
throw new ArgumentNullException(nameof(api));
|
||||
if (httpClient == null)
|
||||
throw new ArgumentNullException(nameof(httpClient));
|
||||
this.api = api;
|
||||
_httpClient = httpClient;
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
public Action<IAPIRequestMaker, RequestMakerState, object> RequestStateChanged
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public async Task<string> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
|
||||
{
|
||||
await default(SynchronizationContextRemover);
|
||||
await api.RateLimit.WaitToProceedAsync();
|
||||
if (url[0] != '/')
|
||||
{
|
||||
url = "/" + url;
|
||||
}
|
||||
string uri2 = (baseUrl ?? api.BaseUrl) + url;
|
||||
if (method == null)
|
||||
{
|
||||
method = api.RequestMethod;
|
||||
}
|
||||
Uri uri = api.ProcessRequestUrl(new UriBuilder(uri2), payload, method);
|
||||
InternalHttpWebRequest request = new InternalHttpWebRequest(uri)
|
||||
{
|
||||
Method = method
|
||||
};
|
||||
request.AddHeader("content-type", api.RequestContentType);
|
||||
request.AddHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36");
|
||||
int num3 = request.Timeout = (request.ReadWriteTimeout = (int)api.RequestTimeout.TotalMilliseconds);
|
||||
await api.ProcessRequestAsync(request, payload);
|
||||
try
|
||||
{
|
||||
RequestStateChanged?.Invoke(this, RequestMakerState.Begin, uri.AbsoluteUri);
|
||||
using var webHttpRequest = request.ToHttpRequestMessage();
|
||||
using var webHttpResponse = await _httpClient.SendAsync(webHttpRequest, _cancellationToken);
|
||||
string text = await webHttpResponse.Content.ReadAsStringAsync();
|
||||
if (!webHttpResponse.IsSuccessStatusCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
throw new APIException($"{webHttpResponse.StatusCode.ConvertInvariant<int>(0)} - {webHttpResponse.StatusCode}");
|
||||
}
|
||||
throw new APIException(text);
|
||||
}
|
||||
api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse));
|
||||
// local reference to handle delegate becoming null, extended discussion here:
|
||||
// https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103
|
||||
var requestStateChanged = RequestStateChanged;
|
||||
if (requestStateChanged != null)
|
||||
{
|
||||
requestStateChanged(this, RequestMakerState.Finished, text);
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
catch (Exception arg)
|
||||
{
|
||||
RequestStateChanged?.Invoke(this, RequestMakerState.Error, arg);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IHasExchangeName
|
||||
{
|
||||
string ExchangeName { get; }
|
||||
}
|
||||
}
|
@ -9,6 +9,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IRateProvider
|
||||
{
|
||||
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken);
|
||||
Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ using Newtonsoft.Json.Linq;
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
// Make sure that only one request is sent to kraken in general
|
||||
public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName
|
||||
public class KrakenExchangeRateProvider : IRateProvider
|
||||
{
|
||||
public KrakenExchangeRateProvider()
|
||||
{
|
||||
@ -33,8 +33,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
public string ExchangeName => "kraken";
|
||||
|
||||
HttpClient _LocalClient;
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
@ -87,9 +85,9 @@ namespace BTCPayServer.Services.Rates
|
||||
{ "ZGBP", "GBP" }
|
||||
};
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new ExchangeRates();
|
||||
var result = new List<PairRate>();
|
||||
var symbols = await GetSymbolsAsync(cancellationToken);
|
||||
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeMarketSymbol(s)).ToList();
|
||||
var csvPairsList = string.Join(",", normalizedPairsList);
|
||||
@ -117,7 +115,7 @@ namespace BTCPayServer.Services.Rates
|
||||
global = await _Helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
|
||||
}
|
||||
if (CurrencyPair.TryParse(global, out var pair))
|
||||
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
result.Add(new PairRate(pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
else
|
||||
notFoundSymbols.TryAdd(symbol, symbol);
|
||||
}
|
||||
@ -127,7 +125,7 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker)
|
||||
|
@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new ExchangeRates());
|
||||
return Task.FromResult(Array.Empty<PairRate>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
26
BTCPayServer.Rating/Providers/PolisRateProvider.cs
Normal file
26
BTCPayServer.Rating/Providers/PolisRateProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class PolisRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public PolisRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["data"].Value<decimal>();
|
||||
return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) };
|
||||
}
|
||||
}
|
||||
}
|
@ -99,6 +99,8 @@ namespace BTCPayServer.Rating
|
||||
RuleList ruleList;
|
||||
|
||||
decimal _Spread;
|
||||
private const string ImplicitSatsRule = "SATS_X = SATS_BTC * BTC_X;\nSATS_BTC = 0.00000001;\n";
|
||||
|
||||
public decimal Spread
|
||||
{
|
||||
get
|
||||
@ -126,6 +128,7 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
public static bool TryParse(string str, out RateRules rules, out List<RateRulesErrors> errors)
|
||||
{
|
||||
str = ImplicitSatsRule + str;
|
||||
rules = null;
|
||||
errors = null;
|
||||
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
|
||||
@ -195,6 +198,7 @@ namespace BTCPayServer.Rating
|
||||
{
|
||||
return root.NormalizeWhitespace("", "\n")
|
||||
.ToFullString()
|
||||
.Replace(ImplicitSatsRule, string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
@ -79,9 +79,9 @@ namespace BTCPayServer.Services.Rates
|
||||
result.Latency = query.Latency;
|
||||
if (query.Exception != null)
|
||||
result.ExchangeExceptions.Add(query.Exception);
|
||||
foreach (var rule in query.ExchangeRates)
|
||||
foreach (var rule in query.PairRates)
|
||||
{
|
||||
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
|
||||
rateRule.ExchangeRates.SetRate(query.Exchange, rule.CurrencyPair, rule.BidAsk);
|
||||
}
|
||||
}
|
||||
rateRule.Reevaluate();
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
try
|
||||
@ -35,7 +35,7 @@ namespace BTCPayServer.Services.Rates
|
||||
catch (Exception ex)
|
||||
{
|
||||
Exception = ex;
|
||||
return new ExchangeRates();
|
||||
return Array.Empty<PairRate>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -46,49 +46,15 @@ namespace BTCPayServer.Services.Rates
|
||||
public class QueryRateResult
|
||||
{
|
||||
public TimeSpan Latency { get; set; }
|
||||
public ExchangeRates ExchangeRates { get; set; }
|
||||
public PairRate[] PairRates { get; set; }
|
||||
public ExchangeException Exception { get; internal set; }
|
||||
public string Exchange { 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
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CacheSpan;
|
||||
}
|
||||
set
|
||||
{
|
||||
_CacheSpan = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -98,95 +64,88 @@ namespace BTCPayServer.Services.Rates
|
||||
return _DirectProviders;
|
||||
}
|
||||
}
|
||||
|
||||
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("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/exchange_rates");
|
||||
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
|
||||
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
|
||||
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
|
||||
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
|
||||
|
||||
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
|
||||
|
||||
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
|
||||
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
|
||||
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
|
||||
}
|
||||
void InitExchanges()
|
||||
{
|
||||
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
|
||||
Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
|
||||
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
|
||||
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
|
||||
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true));
|
||||
Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true));
|
||||
AddExchangeSharpProviders<ExchangeBinanceAPI>("binance");
|
||||
AddExchangeSharpProviders<ExchangeBittrexAPI>("bittrex");
|
||||
AddExchangeSharpProviders<ExchangePoloniexAPI>("poloniex");
|
||||
AddExchangeSharpProviders<ExchangeHitBTCAPI>("hitbtc");
|
||||
AddExchangeSharpProviders<ExchangeNDAXAPI>("ndax");
|
||||
|
||||
// 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("coingecko", new CoinGeckoRateProvider(_httpClientFactory));
|
||||
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")));
|
||||
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
|
||||
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
|
||||
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
|
||||
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
|
||||
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
|
||||
|
||||
// Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking
|
||||
Providers.Add("coinaverage", new CoinGeckoRateProvider(_httpClientFactory));
|
||||
|
||||
AddExchangeSharpProviders<ExchangeBitfinexAPI>("bitfinex");
|
||||
AddExchangeSharpProviders<ExchangeOKExAPI>("okex");
|
||||
AddExchangeSharpProviders<ExchangeCoinbaseAPI>("coinbasepro");
|
||||
// Those exchanges make too many requests, exchange sharp do not parallelize so it is too slow...
|
||||
//AddExchangeSharpProviders<ExchangeGeminiAPI>("gemini");
|
||||
//AddExchangeSharpProviders<ExchangeBitstampAPI>("bitstamp");
|
||||
//AddExchangeSharpProviders<ExchangeBitMEXAPI>("bitmex");
|
||||
|
||||
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);
|
||||
}
|
||||
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
|
||||
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers[provider.Key] = prov;
|
||||
}
|
||||
Providers["gdax"] = Providers["coinbasepro"];
|
||||
|
||||
var cache = new MemoryCache(_CacheOptions);
|
||||
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
|
||||
{
|
||||
if (!Providers.ContainsKey(supportedExchange.Id))
|
||||
if (!Providers.ContainsKey(supportedExchange.Id) && supportedExchange.Id != CoinGeckoRateProvider.CoinGeckoName)
|
||||
{
|
||||
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
|
||||
{
|
||||
Exchange = supportedExchange.Id
|
||||
UnderlyingExchange = supportedExchange.SourceId
|
||||
};
|
||||
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(coingecko);
|
||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IRateProvider AddExchangeSharpProviders<T>(string providerName) where T: ExchangeAPI, new()
|
||||
{
|
||||
var provider = new ExchangeSharpRateProvider<T>(_httpClientFactory.CreateClient($"EXCHANGE_{providerName}".ToUpperInvariant()), true);
|
||||
Providers.Add(providerName, provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
IEnumerable<AvailableRateProvider> _AvailableRateProviders = null;
|
||||
public IEnumerable<AvailableRateProvider> GetSupportedExchanges()
|
||||
{
|
||||
@ -201,91 +160,16 @@ 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(),
|
||||
$"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) });
|
||||
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));
|
||||
}
|
||||
|
||||
private string Normalize(string name)
|
||||
@ -306,8 +190,9 @@ namespace BTCPayServer.Services.Rates
|
||||
var value = await wrapper.GetRatesAsync(cancellationToken);
|
||||
return new QueryRateResult()
|
||||
{
|
||||
Exchange = exchangeName,
|
||||
Latency = wrapper.Latency,
|
||||
ExchangeRates = value,
|
||||
PairRates = value,
|
||||
Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
@ -16,7 +25,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="78.0.3904.7000" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="79.0.3945.3600" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -200,67 +200,27 @@ namespace BTCPayServer.Tests
|
||||
rateProvider.Providers.Clear();
|
||||
|
||||
var coinAverageMock = new MockRateProvider();
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coingecko",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
BidAsk = new BidAsk(5000m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coingecko",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(4500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coingecko",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
|
||||
BidAsk = new BidAsk(162m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coingecko",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m)));
|
||||
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m)));
|
||||
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m)));
|
||||
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m)));
|
||||
rateProvider.Providers.Add("coingecko", coinAverageMock);
|
||||
|
||||
var bitflyerMock = new MockRateProvider();
|
||||
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bitflyer",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
|
||||
BidAsk = new BidAsk(700000m)
|
||||
});
|
||||
bitflyerMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_JPY"), new BidAsk(700000m)));
|
||||
rateProvider.Providers.Add("bitflyer", bitflyerMock);
|
||||
|
||||
var quadrigacx = new MockRateProvider();
|
||||
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "quadrigacx",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
BidAsk = new BidAsk(6000m)
|
||||
});
|
||||
quadrigacx.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(6000m)));
|
||||
rateProvider.Providers.Add("quadrigacx", quadrigacx);
|
||||
|
||||
var bittrex = new MockRateProvider();
|
||||
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bittrex",
|
||||
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
|
||||
BidAsk = new BidAsk(0.004m)
|
||||
});
|
||||
bittrex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("DOGE_BTC"), new BidAsk(0.004m)));
|
||||
rateProvider.Providers.Add("bittrex", bittrex);
|
||||
|
||||
|
||||
var bitfinex = new MockRateProvider();
|
||||
bitfinex.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "bitfinex",
|
||||
CurrencyPair = CurrencyPair.Parse("UST_BTC"),
|
||||
BidAsk = new BidAsk(0.000136m)
|
||||
});
|
||||
bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m)));
|
||||
rateProvider.Providers.Add("bitfinex", bitfinex);
|
||||
}
|
||||
|
||||
|
@ -182,7 +182,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var httpClientFactory = TestUtils.CreateHttpFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
@ -213,7 +213,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = new MockHttpClientFactory();
|
||||
var httpClientFactory = TestUtils.CreateHttpFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
@ -243,12 +243,4 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
public class MockHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name)
|
||||
{
|
||||
return new HttpClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
@ -177,5 +179,43 @@ namespace BTCPayServer.Tests
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseJSModal()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
s.GoToStore(store.storeId);
|
||||
s.AddDerivationScheme();
|
||||
var invoiceId = s.CreateInvoice(store.storeId, 0.001m, "BTC", "a@x.com");
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
s.Driver.Navigate()
|
||||
.GoToUrl(new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}"));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
|
||||
});
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
|
||||
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
|
||||
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
|
||||
new Money(0.001m, MoneyUnit.BTC));
|
||||
|
||||
IWebElement closebutton = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var iframe = s.Driver.SwitchTo().Frame("btcpay");
|
||||
closebutton = iframe.FindElement(By.ClassName("close-action"));
|
||||
Assert.True(closebutton.Displayed);
|
||||
});
|
||||
closebutton.Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
Assert.Equal(s.Driver.Url,
|
||||
new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -10,15 +10,15 @@ namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public List<PairRate> ExchangeRates { get; set; } = new List<PairRate>();
|
||||
|
||||
public MockRateProvider()
|
||||
{
|
||||
|
||||
}
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
return Task.FromResult(ExchangeRates.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ namespace BTCPayServer.Tests
|
||||
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"));
|
||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
@ -80,14 +80,20 @@ namespace BTCPayServer.Tests
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.DerivationScheme, user.ExtKey);
|
||||
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Positive);
|
||||
@ -98,7 +104,7 @@ namespace BTCPayServer.Tests
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
|
||||
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
@ -108,7 +114,7 @@ namespace BTCPayServer.Tests
|
||||
// Can use uploaded file?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
@ -116,17 +122,18 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
Assert.Equal(signedPSBT.ToBase64(), psbt);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string AssertRedirectedPSBT(IActionResult view)
|
||||
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
|
||||
{
|
||||
var postRedirectView = Assert.IsType<ViewResult>(view);
|
||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
||||
Assert.Equal(actionName, postRedirectViewModel.AspAction);
|
||||
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value;
|
||||
return redirectedPSBT;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using Xunit;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -24,6 +25,35 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(1.1m, rule.BidAsk.Ask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanSerializeExchangeRatesCache()
|
||||
{
|
||||
HostedServices.RatesHostedService.ExchangeRatesCache cache = new HostedServices.RatesHostedService.ExchangeRatesCache();
|
||||
cache.Created = DateTimeOffset.UtcNow;
|
||||
cache.States = new List<Services.Rates.BackgroundFetcherState>();
|
||||
cache.States.Add(new Services.Rates.BackgroundFetcherState()
|
||||
{
|
||||
ExchangeName = "Kraken",
|
||||
LastRequested = DateTimeOffset.UtcNow,
|
||||
LastUpdated = DateTimeOffset.UtcNow,
|
||||
Rates = new List<Services.Rates.BackgroundFetcherRate>()
|
||||
{
|
||||
new Services.Rates.BackgroundFetcherRate()
|
||||
{
|
||||
Pair = new CurrencyPair("USD", "BTC"),
|
||||
BidAsk = new BidAsk(1.0m, 2.0m)
|
||||
}
|
||||
}
|
||||
});
|
||||
var str = JsonConvert.SerializeObject(cache, Formatting.Indented);
|
||||
|
||||
var cache2 = JsonConvert.DeserializeObject<HostedServices.RatesHostedService.ExchangeRatesCache>(str);
|
||||
Assert.Equal(cache.Created.ToUnixTimeSeconds(), cache2.Created.ToUnixTimeSeconds());
|
||||
Assert.Equal(cache.States[0].Rates[0].BidAsk, cache2.States[0].Rates[0].BidAsk);
|
||||
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseRateRules()
|
||||
@ -56,6 +86,8 @@ namespace BTCPayServer.Tests
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||
(Pair: "SATS_CAD", Expected: "0.00000001 * coinbase(BTC_CAD)"),
|
||||
(Pair: "Sats_USD", Expected: "0.00000001 * kraken(BTC_USD)")
|
||||
};
|
||||
foreach (var test in tests)
|
||||
{
|
||||
@ -102,6 +134,8 @@ namespace BTCPayServer.Tests
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||
(Pair: "SATS_USD", Expected: "0.00000001 * kraken(BTC_USD)", ExpectedExchangeRates: "kraken(BTC_USD)"),
|
||||
(Pair: "SATS_EUR", Expected: "0.00000001 * coinbase(BTC_EUR)", ExpectedExchangeRates: "coinbase(BTC_EUR)")
|
||||
};
|
||||
foreach (var test in tests2)
|
||||
{
|
||||
@ -189,6 +223,37 @@ namespace BTCPayServer.Tests
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
|
||||
|
||||
// Make sure defining value in sats works
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
builder.AppendLine("BTC_X = coinbase(BTC_X)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("0.00000001 * (6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(0.00006m, rule2.BidAsk.Bid);
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_SATS"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (0.00000001 * (6000, 6100))", rule2.ToString(true));
|
||||
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
|
||||
|
||||
// testing rounding
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
|
||||
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
|
||||
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
|
||||
Assert.Equal(1m / 0.0000000123m, rule2.BidAsk.Ask);
|
||||
Assert.Equal(1m / 0.0000000234m, rule2.BidAsk.Bid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -70,18 +71,18 @@ namespace BTCPayServer.Tests
|
||||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
internal void AssertHappyMessage()
|
||||
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(10_000);
|
||||
using var cts = new CancellationTokenSource(20_000);
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any();
|
||||
var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed);
|
||||
if (success)
|
||||
return;
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
Logs.Tester.LogInformation(this.Driver.PageSource);
|
||||
Assert.True(false, "Should have shown happy message");
|
||||
Assert.True(false, $"Should have shown {severity} message");
|
||||
}
|
||||
|
||||
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
|
||||
@ -202,7 +203,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
internal void AssertNotFound()
|
||||
{
|
||||
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
|
||||
Assert.Contains("404 - Page not found</h1>", Driver.PageSource);
|
||||
}
|
||||
|
||||
public void GoToHome()
|
||||
|
@ -9,6 +9,8 @@ using System.Linq;
|
||||
using NBitcoin;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.Payment;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -415,7 +417,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanManageWallet()
|
||||
{
|
||||
@ -427,22 +429,71 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
|
||||
// to sign the transaction
|
||||
var mnemonic = s.GenerateWallet("BTC", "", true, false);
|
||||
s.GenerateWallet("BTC", "", true, false);
|
||||
|
||||
//let's test quickly the receive wallet page
|
||||
s.Driver.FindElement(By.Id("Wallets")).Click();
|
||||
s.Driver.FindElement(By.LinkText("Manage")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("WalletSend")).Click();
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
//you cant use the Sign with NBX option without saving private keys when generating the wallet.
|
||||
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("WalletReceive")).Click();
|
||||
//generate a receiving address
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
|
||||
var receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
|
||||
//unreserve
|
||||
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
|
||||
//generate it again, should be the same one as before as nothign got used in the meantime
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
|
||||
Assert.Equal( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
|
||||
//send money to addr and ensure it changed
|
||||
|
||||
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
|
||||
sess.ListenAllTrackedSource();
|
||||
var nextEvent = sess.NextEventAsync();
|
||||
s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest),
|
||||
Money.Parse("0.1"));
|
||||
await nextEvent;
|
||||
await Task.Delay(200);
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
s.GoToStore(storeId.storeId);
|
||||
s.GenerateWallet("BTC", "", true, false);
|
||||
s.Driver.FindElement(By.Id("Wallets")).Click();
|
||||
s.Driver.FindElement(By.LinkText("Manage")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletReceive")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
|
||||
|
||||
var invoiceId = s.CreateInvoice(storeId.storeId);
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
var address = invoice.EntityToDTO().Addresses["BTC"];
|
||||
|
||||
|
||||
//wallet should have been imported to bitcoin core wallet in watch only mode.
|
||||
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
||||
Assert.True(result.IsWatchOnly);
|
||||
s.GoToStore(storeId.storeId);
|
||||
mnemonic = s.GenerateWallet("BTC", "", true, true);
|
||||
var mnemonic = s.GenerateWallet("BTC", "", true, true);
|
||||
|
||||
//lets import and save private keys
|
||||
var root = new Mnemonic(mnemonic).DeriveExtKey();
|
||||
invoiceId = s.CreateInvoice(storeId.storeId);
|
||||
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
|
||||
address = invoice.EntityToDTO().Addresses["BTC"];
|
||||
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
||||
//spendable from bitcoin core wallet!
|
||||
Assert.False(result.IsWatchOnly);
|
||||
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
@ -499,7 +550,43 @@ namespace BTCPayServer.Tests
|
||||
checkboxElement.Click();
|
||||
}
|
||||
}
|
||||
|
||||
SignWith(mnemonic);
|
||||
|
||||
s.Driver.FindElement(By.Id("Wallets")).Click();
|
||||
s.Driver.FindElement(By.LinkText("Manage")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletSend")).Click();
|
||||
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(0, jack, 0.01m);
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
|
||||
Assert.Contains(jack.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("0.01000000", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick();
|
||||
Assert.EndsWith("psbt", s.Driver.Url);
|
||||
s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
|
||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
|
||||
Assert.Equal(walletTransactionLink, s.Driver.Url);
|
||||
|
||||
var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21;
|
||||
//let's make bip21 more interesting
|
||||
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
||||
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
||||
s.Driver.FindElement(By.Id("Wallets")).Click();
|
||||
s.Driver.FindElement(By.LinkText("Manage")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletSend")).Click();
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info);
|
||||
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
|
||||
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ using BTCPayServer.Data;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Core;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -43,18 +44,13 @@ namespace BTCPayServer.Tests
|
||||
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
|
||||
var u = await userManager.FindByIdAsync(UserId);
|
||||
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
|
||||
IsAdmin = true;
|
||||
}
|
||||
|
||||
public void Register()
|
||||
{
|
||||
RegisterAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public BitcoinExtKey ExtKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task GrantAccessAsync()
|
||||
{
|
||||
await RegisterAsync();
|
||||
@ -100,26 +96,46 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
|
||||
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false, bool importKeysToNBX = false)
|
||||
{
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
|
||||
return RegisterDerivationSchemeAsync(crytoCode, segwit, importKeysToNBX).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false, bool importKeysToNBX = false)
|
||||
{
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = SupportedNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
|
||||
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
|
||||
{
|
||||
ScriptPubKeyType = segwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy,
|
||||
SavePrivateKeys = importKeysToNBX
|
||||
});
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
Enabled = true,
|
||||
CryptoCode = cryptoCode,
|
||||
Network = SupportedNetwork,
|
||||
RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(),
|
||||
RootKeyPath = SupportedNetwork.GetRootKeyPath(),
|
||||
Source = "NBXplorer",
|
||||
AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(),
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
|
||||
|
||||
public DerivationStrategyBase DerivationScheme
|
||||
{
|
||||
get
|
||||
{
|
||||
return GenerateWalletResponseV.DerivationScheme;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
|
@ -7,6 +7,8 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Xunit.Sdk;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -104,5 +106,12 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static IHttpClientFactory CreateHttpFactory()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddHttpClient();
|
||||
return services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ namespace BTCPayServer.Tests
|
||||
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
|
||||
|
||||
Assert.NotEmpty(addDeviceVM.Challenge);
|
||||
Assert.Equal(addDeviceVM.Name, "testdevice");
|
||||
Assert.Equal("testdevice", addDeviceVM.Name);
|
||||
Assert.NotEmpty(addDeviceVM.Version);
|
||||
Assert.Null(addDeviceVM.DeviceResponse);
|
||||
|
||||
|
@ -150,17 +150,19 @@ namespace BTCPayServer.Tests
|
||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null),
|
||||
new LightningLikePaymentHandler(null, null, networkProvider, null),
|
||||
});
|
||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||
var networkLTC = networkProvider.GetNetwork("LTC");
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Networks = networkProvider;
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.ProductInformation = new ProductInformation() {Price = 100};
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() {CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails(
|
||||
paymentMethods.Add(new PaymentMethod() {Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod() {CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails(
|
||||
paymentMethods.Add(new PaymentMethod() {Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
NextNetworkFee = Money.Coins(0.00010000m), DepositAddress = dummy
|
||||
@ -173,7 +175,7 @@ namespace BTCPayServer.Tests
|
||||
invoiceEntity.Payments.Add(
|
||||
new PaymentEntity()
|
||||
{
|
||||
|
||||
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
@ -315,7 +317,7 @@ namespace BTCPayServer.Tests
|
||||
entity.Payments.Add(
|
||||
new PaymentEntity()
|
||||
{
|
||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||
Accounted = true
|
||||
});
|
||||
|
||||
@ -330,15 +332,15 @@ namespace BTCPayServer.Tests
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Rate = 1000,
|
||||
CryptoCode = "BTC",
|
||||
Rate = 1000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Rate = 500,
|
||||
CryptoCode = "LTC",
|
||||
Rate = 500,
|
||||
NextNetworkFee = Money.Coins(0.01m)
|
||||
});
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
@ -521,6 +523,39 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanThrowBitpay404Error()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
try
|
||||
{
|
||||
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
|
||||
}
|
||||
catch (BitPayException ex)
|
||||
{
|
||||
Assert.Equal("Object not found", ex.Errors.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
@ -819,7 +854,7 @@ namespace BTCPayServer.Tests
|
||||
var walletId = new WalletId(acc.StoreId, "BTC");
|
||||
acc.IsAdmin = true;
|
||||
walletController = acc.GetController<WalletsController>();
|
||||
|
||||
|
||||
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
|
||||
Assert.True(rescan.Ok);
|
||||
Assert.True(rescan.IsFullySync);
|
||||
@ -862,6 +897,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"));
|
||||
@ -1787,7 +1823,7 @@ namespace BTCPayServer.Tests
|
||||
string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
|
||||
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
|
||||
|
||||
// And with a good file? (upub)
|
||||
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
@ -1969,15 +2005,15 @@ donation:
|
||||
Assert.Equal(10.00m, orangeInvoice.Price);
|
||||
Assert.Equal("CAD", orangeInvoice.Currency);
|
||||
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
||||
|
||||
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
|
||||
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
|
||||
@ -2025,8 +2061,8 @@ donation:
|
||||
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
|
||||
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//test inventory related features
|
||||
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
|
||||
vmpos.Title = "hello";
|
||||
@ -2039,13 +2075,13 @@ inventoryitem:
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
|
||||
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
|
||||
|
||||
|
||||
//inventoryitem has unlimited items available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
@ -2055,7 +2091,7 @@ noninventoryitem:
|
||||
Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem")));
|
||||
var inventoryItemInvoice = Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
|
||||
Assert.NotNull(inventoryItemInvoice);
|
||||
|
||||
|
||||
//let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock
|
||||
var controller = tester.PayTester.GetController<InvoiceController>(user.UserId, user.StoreId);
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
@ -2067,7 +2103,7 @@ noninventoryitem:
|
||||
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
|
||||
Assert.Equal(1, appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -2250,7 +2286,7 @@ noninventoryitem:
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
//
|
||||
//
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||
@ -2680,21 +2716,18 @@ noninventoryitem:
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct).Select(s => s.Id).ToHashSet();
|
||||
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.UnderlyingExchange != null))
|
||||
.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;
|
||||
var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
|
||||
result.Fetcher.InvalidateCache();
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
@ -2704,6 +2737,11 @@ noninventoryitem:
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
|
||||
}
|
||||
else if (result.ExpectedName == "polispay")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS
|
||||
}
|
||||
else
|
||||
{
|
||||
// This check if the currency pair is using right currency pair
|
||||
@ -2715,6 +2753,12 @@ noninventoryitem:
|
||||
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
|
||||
);
|
||||
}
|
||||
// We are not showing a directly implemented exchange as directly implemented in the UI
|
||||
// we need to modify the AvailableRateProvider
|
||||
|
||||
// There are some exception we stopped supporting but don't want to break backward compat
|
||||
if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax")
|
||||
Assert.Contains(result.ExpectedName, directlySupported);
|
||||
}
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
@ -2730,7 +2774,7 @@ noninventoryitem:
|
||||
await provider.GetRatesAsync(default);
|
||||
var state = provider.GetState();
|
||||
Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR"));
|
||||
var provider2 = new BackgroundFetcherRateProvider("kraken", provider.Inner)
|
||||
var provider2 = new BackgroundFetcherRateProvider(provider.Inner)
|
||||
{
|
||||
RefreshRate = provider.RefreshRate,
|
||||
ValidatyTime = provider.ValidatyTime
|
||||
@ -2749,7 +2793,6 @@ noninventoryitem:
|
||||
// Should not throw, as things should be cached
|
||||
await provider2.GetRatesAsync(cts.Token);
|
||||
}
|
||||
Assert.Equal(provider.ExchangeName, provider2.ExchangeName);
|
||||
Assert.Equal(provider.NextUpdate, provider2.NextUpdate);
|
||||
Assert.NotEqual(provider.LastRequested, provider2.LastRequested);
|
||||
Assert.Equal(provider.Expiration, provider2.Expiration);
|
||||
@ -2785,23 +2828,18 @@ 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(TestUtils.CreateHttpFactory());
|
||||
}
|
||||
|
||||
class SpyRateProvider : IRateProvider
|
||||
{
|
||||
public bool Hit { get; set; }
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Hit = true;
|
||||
var rates = new ExchangeRates();
|
||||
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
|
||||
return Task.FromResult(rates);
|
||||
var rates = new List<PairRate>();
|
||||
rates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
|
||||
return Task.FromResult(rates.ToArray());
|
||||
}
|
||||
|
||||
public void AssertHit()
|
||||
@ -2889,47 +2927,33 @@ noninventoryitem:
|
||||
return name;
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CanCreateSqlitedb()
|
||||
{
|
||||
if (File.Exists("temp.db"))
|
||||
File.Delete("temp.db");
|
||||
// This test sqlite can migrate
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
builder.UseSqlite("Data Source=temp.db");
|
||||
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
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);
|
||||
var fetch = new BackgroundFetcherRateProvider(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();
|
||||
@ -2975,7 +2999,7 @@ noninventoryitem:
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
@ -2996,8 +3020,8 @@ noninventoryitem:
|
||||
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
|
||||
|
||||
|
||||
|
||||
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
|
||||
{
|
||||
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
|
||||
@ -3008,13 +3032,13 @@ noninventoryitem:
|
||||
}}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
|
||||
@ -3035,7 +3059,7 @@ noninventoryitem:
|
||||
})).ActionName);
|
||||
|
||||
var manageController = user.GetController<ManageController>();
|
||||
|
||||
|
||||
//by default no u2f devices available
|
||||
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
var addRequest = Assert.IsType<AddU2FDeviceViewModel>(Assert.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
|
||||
@ -3064,10 +3088,10 @@ noninventoryitem:
|
||||
};
|
||||
await context.U2FDevices.AddAsync(newDevice);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
||||
Assert.NotNull(newDevice.Id);
|
||||
Assert.NotEmpty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
|
||||
|
||||
}
|
||||
|
||||
//check if we are showing the u2f login screen now
|
||||
@ -3081,10 +3105,10 @@ noninventoryitem:
|
||||
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
|
||||
//2fa was never enabled for user so this should be empty
|
||||
Assert.Null(vm.LoginWith2FaViewModel);
|
||||
Assert.NotNull(vm.LoginWithU2FViewModel);
|
||||
Assert.NotNull(vm.LoginWithU2FViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
|
@ -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: |
|
||||
@ -76,7 +76,7 @@ services:
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.1.5
|
||||
image: nicolasdorier/nbxplorer:2.1.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -98,6 +98,8 @@ services:
|
||||
NBXPLORER_LBTCRPCUSER: "liquid"
|
||||
NBXPLORER_LBTCRPCPASSWORD: "liquid"
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_MINGAPSIZE: 5
|
||||
NBXPLORER_MAXGAPSIZE: 10
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
@ -108,7 +110,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 +259,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 +289,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"
|
||||
|
@ -69,8 +69,8 @@
|
||||
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
|
||||
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
|
||||
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -141,6 +141,7 @@ namespace BTCPayServer.Configuration
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
ExternalServices.LoadNonCryptoServices(conf);
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
|
||||
var services = conf.GetOrDefault<string>("externalservices", null);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
@ -46,7 +47,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest || serviceType == ExternalServiceTypes.CLightningRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
@ -77,7 +78,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
if (new []{ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator}.Contains(serviceType))
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
@ -94,7 +95,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
@ -144,9 +145,7 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
public bool? IsOnion()
|
||||
{
|
||||
if (this.Server == null || !this.Server.IsAbsoluteUri)
|
||||
return null;
|
||||
return this.Server.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
return Server?.IsOnion();
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
|
@ -35,17 +35,30 @@ namespace BTCPayServer.Configuration
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
"Ride the Lightning server");
|
||||
Load(configuration, cryptoCode, "clightningrest", ExternalServiceTypes.CLightningRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/clightning-rest/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning REST");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
|
||||
}
|
||||
|
||||
public void LoadNonCryptoServices(IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, null, "configurator", ExternalServiceTypes.Configurator, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"configurator: 'cookiefilepathfile=/etc/configurator/cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"Configurator");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var setting = $"{(!string.IsNullOrEmpty(cryptoCode)? $"{cryptoCode}.": string.Empty)}external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
@ -65,8 +78,11 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
return this.FirstOrDefault(o =>
|
||||
(cryptoCode == null && o.CryptoCode == null) ||
|
||||
(o.CryptoCode != null && o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase))
|
||||
&&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +104,8 @@ namespace BTCPayServer.Configuration
|
||||
RTL,
|
||||
Charge,
|
||||
P2P,
|
||||
RPC
|
||||
RPC,
|
||||
Configurator,
|
||||
CLightningRest
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ namespace BTCPayServer.Controllers
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Title = settings.Title,
|
||||
StoreId = app.StoreDataId,
|
||||
Enabled = settings.Enabled,
|
||||
@ -61,8 +60,8 @@ namespace BTCPayServer.Controllers
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
@ -70,9 +69,9 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
if (!string.IsNullOrEmpty(vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
@ -98,14 +97,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && !parsedSounds.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
|
||||
}
|
||||
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
@ -114,13 +113,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
|
||||
}
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
@ -139,7 +138,6 @@ namespace BTCPayServer.Controllers
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
@ -157,24 +155,16 @@ namespace BTCPayServer.Controllers
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
app.SetSettings(newSettings);
|
||||
|
||||
if (command == "save")
|
||||
{
|
||||
await _AppService.UpdateOrCreateApp(app);
|
||||
await _AppService.UpdateOrCreateApp(app);
|
||||
|
||||
_EventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
|
||||
}
|
||||
else if (command == "viewapp")
|
||||
_EventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
return RedirectToAction(nameof(AppsPublicController.ViewCrowdfund), "AppsPublic", new { appId });
|
||||
}
|
||||
return NotFound();
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,11 +79,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
|
||||
public string EmbeddedCSS { get; set; }
|
||||
|
||||
|
||||
public string Description { get; set; }
|
||||
public string NotificationEmail { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
}
|
||||
@ -96,10 +95,9 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Id = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Title = settings.Title,
|
||||
@ -116,10 +114,9 @@ namespace BTCPayServer.Controllers
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
Description = settings.Description,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
SearchTerm = $"storeid:{app.StoreDataId}",
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : ""
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
@ -194,11 +191,10 @@ namespace BTCPayServer.Controllers
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
Description = vm.Description,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
});
|
||||
await _AppService.UpdateOrCreateApp(app);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
@ -211,8 +207,8 @@ namespace BTCPayServer.Controllers
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
|
@ -177,7 +177,6 @@ namespace BTCPayServer.Controllers
|
||||
OrderId = orderId,
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
@ -306,7 +305,6 @@ namespace BTCPayServer.Controllers
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ??
|
||||
|
27
BTCPayServer/Controllers/ErrorController.cs
Normal file
27
BTCPayServer/Controllers/ErrorController.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("[controller]/[action]")]
|
||||
public class ErrorController : Controller
|
||||
{
|
||||
public IActionResult Handle(int? statusCode = null)
|
||||
{
|
||||
if (statusCode.HasValue)
|
||||
{
|
||||
var specialPages = new[] { 404, 429, 500 };
|
||||
if (specialPages.Any(a => a == statusCode.Value))
|
||||
{
|
||||
var viewName = statusCode.ToString();
|
||||
return View(viewName);
|
||||
}
|
||||
}
|
||||
return View(statusCode);
|
||||
}
|
||||
}
|
||||
}
|
@ -65,7 +65,6 @@ namespace BTCPayServer.Controllers
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -104,7 +103,6 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
|
||||
var paymentMethodDetails = data.GetPaymentMethodDetails();
|
||||
cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination();
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
@ -276,7 +274,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,
|
||||
@ -539,7 +537,6 @@ namespace BTCPayServer.Controllers
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationEmail = model.NotificationEmail,
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
|
@ -26,6 +26,15 @@ namespace BTCPayServer.Controllers
|
||||
private InvoiceController _InvoiceController;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
[HttpGet]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[Route("api/v1/invoices")]
|
||||
public async Task<IActionResult> PayButtonHandle(PayButtonViewModel model)
|
||||
{
|
||||
return await PayButtonHandle(model, CancellationToken.None);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("api/v1/invoices")]
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
@ -49,17 +58,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)
|
||||
{
|
||||
@ -598,10 +528,12 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
|
||||
|
||||
[Route("server/services/{serviceName}/{cryptoCode?}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
|
||||
if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
@ -612,6 +544,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
if (service.Type == ExternalServiceTypes.P2P)
|
||||
{
|
||||
return View("P2PService", new LightningWalletServices()
|
||||
@ -662,9 +595,19 @@ namespace BTCPayServer.Controllers
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.CLightningRest:
|
||||
return LndServices(service, connectionString, nonce, "CLightningRestServices");
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
case ExternalServiceTypes.Configurator:
|
||||
return View("ConfiguratorService",
|
||||
new LightningWalletServices()
|
||||
{
|
||||
ShowQR = showQR,
|
||||
WalletName = service.ServiceName,
|
||||
ServiceLink = $"{connectionString.Server}?password={connectionString.AccessKey}"
|
||||
});
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
@ -733,7 +676,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce, string view = nameof(LndServices))
|
||||
{
|
||||
var model = new LndServicesViewModel();
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
@ -743,7 +686,7 @@ namespace BTCPayServer.Controllers
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
|
||||
{
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
@ -772,7 +715,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return View(nameof(LndServices), model);
|
||||
return View(view, model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
@ -824,10 +767,10 @@ namespace BTCPayServer.Controllers
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Type = service.Type == ExternalServiceTypes.LNDRest? "lnd-rest": "clightning-rest";
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
@ -848,7 +791,6 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
|
||||
[Route("server/services/dynamic-dns")]
|
||||
public async Task<IActionResult> DynamicDnsServices()
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@ -267,6 +268,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
_EventAggregator.Publish(new WalletChangedEvent()
|
||||
{
|
||||
WalletId = new WalletId(storeId, cryptoCode)
|
||||
});
|
||||
|
||||
if (willBeExcluded != wasExcluded)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
|
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
SettingsRepository settingsRepository,
|
||||
IAuthorizationService authorizationService,
|
||||
EventAggregator eventAggregator,
|
||||
CssThemeManager cssThemeManager)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
@ -74,6 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
_settingsRepository = settingsRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
@ -100,6 +102,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly CssThemeManager _CssThemeManager;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
|
||||
[TempData]
|
||||
public bool StoreNotConfigured
|
||||
@ -496,13 +499,16 @@ namespace BTCPayServer.Controllers
|
||||
case BitcoinPaymentType _:
|
||||
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
|
||||
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||
var value = strategy?.ToPrettyString() ?? string.Empty;
|
||||
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = paymentMethodId.CryptoCode,
|
||||
WalletSupported = network.WalletSupported,
|
||||
Value = strategy?.ToPrettyString() ?? string.Empty,
|
||||
Value = value,
|
||||
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null
|
||||
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null,
|
||||
Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
|
||||
});
|
||||
break;
|
||||
case LightningPaymentType _:
|
||||
@ -641,6 +647,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}/revoke")]
|
||||
public async Task<IActionResult> RevokeToken(string tokenId)
|
||||
@ -667,7 +674,7 @@ namespace BTCPayServer.Controllers
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token";
|
||||
else
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -774,13 +781,22 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId)
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId, string command="")
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
|
||||
if (command == "revoke")
|
||||
{
|
||||
await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key revoked";
|
||||
}
|
||||
else
|
||||
{
|
||||
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId
|
||||
|
@ -7,6 +7,7 @@ using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -19,7 +20,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
|
||||
foreach (var transactionOutput in sendModel.Outputs)
|
||||
{
|
||||
var psbtDestination = new CreatePSBTDestination();
|
||||
@ -56,12 +56,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
vm.CryptoCode = network.CryptoCode;
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
|
||||
WellknownMetadataKeys.Mnemonic));
|
||||
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
|
||||
{
|
||||
vm.Decoded = psbt.ToString();
|
||||
vm.PSBT = psbt.ToBase64();
|
||||
}
|
||||
return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
|
||||
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
@ -93,7 +96,7 @@ namespace BTCPayServer.Controllers
|
||||
case "vault":
|
||||
return ViewVault(walletId, psbt);
|
||||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt);
|
||||
return ViewWalletSendLedger(walletId, psbt);
|
||||
case "update":
|
||||
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network);
|
||||
@ -103,12 +106,25 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
|
||||
return RedirectToWalletPSBT(walletId, psbt, vm.FileName);
|
||||
return RedirectToWalletPSBT(psbt, vm.FileName);
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, psbt.ToBase64());
|
||||
case "nbx-seed":
|
||||
if (await CanUseHotWallet())
|
||||
{
|
||||
var derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
|
||||
return await SignWithSeed(walletId,
|
||||
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()});
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
case "broadcast":
|
||||
{
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64());
|
||||
return RedirectToWalletPSBTReady(psbt.ToBase64());
|
||||
}
|
||||
case "combine":
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
@ -145,7 +161,6 @@ namespace BTCPayServer.Controllers
|
||||
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
|
||||
vm.SigningKey = signingKey;
|
||||
vm.SigningKeyPath = signingKeyPath;
|
||||
|
||||
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
@ -207,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.CanCalculateBalance = true;
|
||||
vm.Positive = balanceChange >= Money.Zero;
|
||||
}
|
||||
|
||||
vm.Inputs = new List<WalletPSBTReadyViewModel.InputViewModel>();
|
||||
foreach (var input in psbtObject.Inputs)
|
||||
{
|
||||
var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
|
||||
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
inputVm.Positive = balanceChange2 >= Money.Zero;
|
||||
inputVm.Index = (int)input.Index;
|
||||
}
|
||||
|
||||
vm.Destinations = new List<WalletPSBTReadyViewModel.DestinationViewModel>();
|
||||
foreach (var output in psbtObject.Outputs)
|
||||
{
|
||||
var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
|
||||
@ -280,14 +295,14 @@ namespace BTCPayServer.Controllers
|
||||
catch
|
||||
{
|
||||
vm.GlobalError = "Invalid PSBT";
|
||||
return View(vm);
|
||||
return View(nameof(WalletPSBTReady),vm);
|
||||
}
|
||||
if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
vm.SetErrors(errors);
|
||||
return View(vm);
|
||||
return View(nameof(WalletPSBTReady),vm);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
@ -296,24 +311,24 @@ namespace BTCPayServer.Controllers
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
|
||||
return View(vm);
|
||||
return View(nameof(WalletPSBTReady),vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.GlobalError = "Error while broadcasting: " + ex.Message;
|
||||
return View(vm);
|
||||
return View(nameof(WalletPSBTReady),vm);
|
||||
}
|
||||
return RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return RedirectToWalletPSBT(walletId, psbt);
|
||||
return RedirectToWalletPSBT(psbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.GlobalError = "Unknown command";
|
||||
return View(vm);
|
||||
return View(nameof(WalletPSBTReady),vm);
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,7 +357,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!";
|
||||
return RedirectToWalletPSBT(walletId, sourcePSBT);
|
||||
return RedirectToWalletPSBT(sourcePSBT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
@ -12,24 +11,21 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Controllers.StoresController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -49,6 +45,9 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
public RateFetcher RateFetcher { get; }
|
||||
|
||||
CurrencyNameTable _currencyTable;
|
||||
@ -63,7 +62,10 @@ namespace BTCPayServer.Controllers
|
||||
IAuthorizationService authorizationService,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletReceiveStateService walletReceiveStateService,
|
||||
EventAggregator eventAggregator,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
@ -77,6 +79,9 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_WalletReceiveStateService = walletReceiveStateService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
@ -231,6 +236,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}")]
|
||||
[Route("{walletId}/transactions")]
|
||||
public async Task<IActionResult> WalletTransactions(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string labelFilter = null)
|
||||
@ -294,6 +300,79 @@ namespace BTCPayServer.Controllers
|
||||
return $"{walletId}:{txId}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/receive")]
|
||||
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
var address = _WalletReceiveStateService.Get(walletId)?.Address;
|
||||
return View(new WalletReceiveViewModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
Address = address?.ToString(),
|
||||
CryptoImage = GetImage(paymentMethod.PaymentId, network)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/receive")]
|
||||
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletReceiveViewModel viewModel, string command)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
switch (command)
|
||||
{
|
||||
case "unreserve-current-address":
|
||||
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
|
||||
if (cachedAddress == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
|
||||
ExplorerClientProvider.GetExplorerClient(network)
|
||||
.CancelReservation(cachedAddress.DerivationStrategy, new[] {cachedAddress.KeyPath});
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
AllowDismiss = true,
|
||||
Message = $"Address {address} was unreserved.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
});
|
||||
_WalletReceiveStateService.Remove(walletId);
|
||||
break;
|
||||
case "generate-new-address":
|
||||
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
|
||||
_WalletReceiveStateService.Set(walletId, reserve);
|
||||
break;
|
||||
}
|
||||
return RedirectToAction(nameof(WalletReceive), new {walletId});
|
||||
}
|
||||
|
||||
private async Task<bool> CanUseHotWallet()
|
||||
{
|
||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
|
||||
if (isAdmin)
|
||||
return true;
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
return policies?.AllowHotWalletForAll is true;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
@ -331,6 +410,9 @@ namespace BTCPayServer.Controllers
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey));
|
||||
model.CurrentBalance = await balance;
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
@ -356,12 +438,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
|
||||
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -372,6 +455,13 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null || network.ReadonlyWallet)
|
||||
return NotFound();
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
LoadFromBIP21(vm, bip21, network);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
decimal transactionAmountSum = 0;
|
||||
|
||||
if (command == "add-output")
|
||||
@ -387,7 +477,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.Outputs.RemoveAt(index);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
if (!vm.Outputs.Any())
|
||||
{
|
||||
@ -480,20 +569,70 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
case "vault":
|
||||
return ViewVault(walletId, psbt.PSBT);
|
||||
case "nbx-seed":
|
||||
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation);
|
||||
|
||||
return await SignWithSeed(walletId, new SignWithSeedViewModel()
|
||||
{
|
||||
SeedOrKey = extKey,
|
||||
PSBT = psbt.PSBT.ToBase64()
|
||||
});
|
||||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
return ViewWalletSendLedger(walletId, psbt.PSBT, psbt.ChangeAddress);
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
|
||||
case "analyze-psbt":
|
||||
var name =
|
||||
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
|
||||
return RedirectToWalletPSBT(walletId, psbt.PSBT, name);
|
||||
return RedirectToWalletPSBT(psbt.PSBT, name);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}";
|
||||
}
|
||||
|
||||
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
|
||||
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
|
||||
DestinationAddress = uriBuilder.Address.ToString(),
|
||||
SubtractFeesFromOutput = false
|
||||
}
|
||||
};
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Html =
|
||||
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "The provided BIP21 payment URI was malformed"
|
||||
});
|
||||
}
|
||||
|
||||
ModelState.Clear();
|
||||
}
|
||||
|
||||
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
|
||||
{
|
||||
return View("WalletSendVault", new WalletSendVaultModel()
|
||||
@ -504,7 +643,31 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null)
|
||||
[HttpPost]
|
||||
[Route("{walletId}/vault")]
|
||||
public async Task<IActionResult> SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendVaultModel model)
|
||||
{
|
||||
|
||||
return RedirectToWalletPSBTReady(model.PSBT);
|
||||
}
|
||||
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null)
|
||||
{
|
||||
var vm = new PostRedirectViewModel()
|
||||
{
|
||||
AspController = "Wallets",
|
||||
AspAction = nameof(WalletPSBTReady),
|
||||
Parameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("psbt", psbt),
|
||||
new KeyValuePair<string, string>("SigningKey", signingKey),
|
||||
new KeyValuePair<string, string>("SigningKeyPath", signingKeyPath)
|
||||
}
|
||||
};
|
||||
return View("PostRedirect", vm);
|
||||
}
|
||||
|
||||
private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null)
|
||||
{
|
||||
var vm = new PostRedirectViewModel()
|
||||
{
|
||||
@ -542,17 +705,25 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||
private ViewResult ViewWalletSendLedger(WalletId walletId, PSBT psbt, BitcoinAddress hintChange = null)
|
||||
{
|
||||
SetAmbientPSBT(psbt);
|
||||
return View("WalletSendLedger", new WalletSendLedgerModel()
|
||||
{
|
||||
PSBT = psbt.ToBase64(),
|
||||
HintChange = hintChange?.ToString(),
|
||||
WebsocketPath = this.Url.Action(nameof(LedgerConnection))
|
||||
WebsocketPath = this.Url.Action(nameof(LedgerConnection), new { walletId = walletId.ToString() })
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/ledger")]
|
||||
public async Task<IActionResult> SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendLedgerModel model)
|
||||
{
|
||||
return RedirectToWalletPSBTReady(model.PSBT);
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/psbt/seed")]
|
||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,string psbt)
|
||||
@ -569,7 +740,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
return View("SignWithSeed", viewModel);
|
||||
}
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
@ -592,7 +763,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
return View("SignWithSeed", viewModel);
|
||||
}
|
||||
|
||||
ExtKey signingKey = null;
|
||||
@ -605,7 +776,7 @@ namespace BTCPayServer.Controllers
|
||||
if (rootedKeyPath == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
|
||||
return View(viewModel);
|
||||
return View("SignWithSeed", viewModel);
|
||||
}
|
||||
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
|
||||
if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
|
||||
@ -626,7 +797,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(viewModel);
|
||||
}
|
||||
ModelState.Remove(nameof(viewModel.PSBT));
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
|
||||
return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
|
||||
}
|
||||
|
||||
private bool PSBTChanged(PSBT psbt, Action act)
|
||||
@ -957,6 +1128,23 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
? Url.Content(network.CryptoImagePath)
|
||||
: Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletReceiveViewModel
|
||||
{
|
||||
public string CryptoImage { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
10
BTCPayServer/Events/NewOnChainTransactionEvent.cs
Normal file
10
BTCPayServer/Events/NewOnChainTransactionEvent.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NewOnChainTransactionEvent
|
||||
{
|
||||
public NewTransactionEvent NewTransactionEvent { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
}
|
7
BTCPayServer/Events/WalletChangedEvent.cs
Normal file
7
BTCPayServer/Events/WalletChangedEvent.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class WalletChangedEvent
|
||||
{
|
||||
public WalletId WalletId { get; set; }
|
||||
}
|
||||
}
|
@ -186,11 +186,19 @@ namespace BTCPayServer
|
||||
|
||||
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
if (IsSegwitCore(derivationStrategyBase))
|
||||
return true;
|
||||
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
|
||||
return ScriptPubKeyType(derivationStrategyBase) != NBitcoin.ScriptPubKeyType.Legacy;
|
||||
}
|
||||
public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
if (IsSegwitCore(derivationStrategyBase))
|
||||
{
|
||||
return NBitcoin.ScriptPubKeyType.Segwit;
|
||||
}
|
||||
|
||||
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner))
|
||||
? NBitcoin.ScriptPubKeyType.SegwitP2SH
|
||||
: NBitcoin.ScriptPubKeyType.Legacy;
|
||||
}
|
||||
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
|
||||
{
|
||||
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
|
||||
@ -257,6 +265,12 @@ namespace BTCPayServer
|
||||
return false;
|
||||
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsOnion(this Uri uri)
|
||||
{
|
||||
return uri?.DnsSafeHost?.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) is true;
|
||||
}
|
||||
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
|
@ -128,6 +128,7 @@ namespace BTCPayServer.HostedServices
|
||||
emailBody);
|
||||
|
||||
}
|
||||
|
||||
if (invoice.NotificationURL != null)
|
||||
{
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
|
||||
|
@ -54,7 +54,7 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@ -180,7 +180,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
_WatchRequests.Writer.TryWrite(invoiceId);
|
||||
|
||||
if (!_WatchRequests.Writer.TryWrite(invoiceId))
|
||||
{
|
||||
Logs.PayServer.LogWarning($"Failed to write invoice {invoiceId} into WatchRequests channel");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId)
|
||||
@ -188,13 +192,16 @@ namespace BTCPayServer.HostedServices
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
try
|
||||
{
|
||||
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
// add 1 second to ensure watch won't trigger moments before invoice expires
|
||||
var delay = invoice.ExpirationTime.AddSeconds(1) - DateTimeOffset.UtcNow;
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
|
||||
|
||||
// add 1 second to ensure watch won't trigger moments before monitoring expires
|
||||
delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow;
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
@ -54,13 +50,20 @@ namespace BTCPayServer.HostedServices
|
||||
return fetcher.LastRequested is DateTimeOffset v &&
|
||||
DateTimeOffset.UtcNow - v < TimeSpan.FromDays(1.0);
|
||||
}
|
||||
|
||||
IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders()
|
||||
{
|
||||
foreach (var kv in _RateProviderFactory.Providers)
|
||||
{
|
||||
if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher))
|
||||
{
|
||||
yield return (kv.Key, fetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
async Task RefreshRates()
|
||||
{
|
||||
var usedProviders = _RateProviderFactory.Providers
|
||||
.Select(p => p.Value)
|
||||
.OfType<BackgroundFetcherRateProvider>()
|
||||
.Where(IsStillUsed)
|
||||
.ToArray();
|
||||
var usedProviders = GetStillUsedProviders().ToArray();
|
||||
if (usedProviders.Length == 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
|
||||
@ -72,11 +75,11 @@ namespace BTCPayServer.HostedServices
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(usedProviders
|
||||
.Select(p => p.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
|
||||
Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}");
|
||||
}
|
||||
}, TaskScheduler.Default))
|
||||
.ToArray()).WithCancellation(timeout.Token);
|
||||
@ -112,20 +115,27 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task TryLoadRateCache()
|
||||
{
|
||||
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
|
||||
if (cache != null)
|
||||
try
|
||||
{
|
||||
_LastCacheDate = cache.Created;
|
||||
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
|
||||
foreach (var obj in _RateProviderFactory.Providers
|
||||
.Select(p => p.Value)
|
||||
.OfType<BackgroundFetcherRateProvider>()
|
||||
.Select(v => (Fetcher: v, State: stateByExchange.TryGet(v.ExchangeName)))
|
||||
.Where(v => v.State != null))
|
||||
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
|
||||
if (cache != null)
|
||||
{
|
||||
obj.Fetcher.LoadState(obj.State);
|
||||
_LastCacheDate = cache.Created;
|
||||
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
|
||||
foreach (var provider in _RateProviderFactory.Providers)
|
||||
{
|
||||
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
|
||||
provider.Value is BackgroundFetcherRateProvider fetcher)
|
||||
{
|
||||
fetcher.LoadState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, "Warning: Error while trying to load rates from cache");
|
||||
}
|
||||
}
|
||||
|
||||
DateTimeOffset? _LastCacheDate;
|
||||
@ -134,28 +144,16 @@ namespace BTCPayServer.HostedServices
|
||||
var cache = new ExchangeRatesCache();
|
||||
cache.Created = DateTimeOffset.UtcNow;
|
||||
_LastCacheDate = cache.Created;
|
||||
cache.States = _RateProviderFactory.Providers
|
||||
.Select(p => p.Value)
|
||||
.OfType<BackgroundFetcherRateProvider>()
|
||||
.Where(IsStillUsed)
|
||||
.Select(p => p.GetState())
|
||||
.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))
|
||||
var usedProviders = GetStillUsedProviders().ToArray();
|
||||
cache.States = new List<BackgroundFetcherState>(usedProviders.Length);
|
||||
foreach (var provider in usedProviders)
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
var state = provider.Fetcher.GetState();
|
||||
state.ExchangeName = provider.ExchangeName;
|
||||
cache.States.Add(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
|
||||
await _SettingsRepository.UpdateSetting(cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
BTCPayServer/HostedServices/WalletReceiveCacheUpdater.cs
Normal file
51
BTCPayServer/HostedServices/WalletReceiveCacheUpdater.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class WalletReceiveCacheUpdater : IHostedService
|
||||
{
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
||||
|
||||
private readonly CompositeDisposable _Leases = new CompositeDisposable();
|
||||
|
||||
public WalletReceiveCacheUpdater(EventAggregator eventAggregator,
|
||||
WalletReceiveStateService walletReceiveStateService)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_WalletReceiveStateService = walletReceiveStateService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Leases.Add(_EventAggregator.Subscribe<WalletChangedEvent>(evt =>
|
||||
_WalletReceiveStateService.Remove(evt.WalletId)));
|
||||
|
||||
_Leases.Add(_EventAggregator.Subscribe<NewOnChainTransactionEvent>(evt =>
|
||||
{
|
||||
var matching = _WalletReceiveStateService
|
||||
.GetByDerivation(evt.CryptoCode, evt.NewTransactionEvent.DerivationStrategy).Where(pair =>
|
||||
evt.NewTransactionEvent.Outputs.Any(output => output.ScriptPubKey == pair.Value.ScriptPubKey));
|
||||
|
||||
foreach (var keyValuePair in matching)
|
||||
{
|
||||
_WalletReceiveStateService.Remove(keyValuePair.Key);
|
||||
}
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Leases.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>();
|
||||
@ -178,6 +177,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<PaymentRequestRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<WalletReceiveStateService>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
@ -217,6 +217,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>();
|
||||
@ -259,10 +260,20 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var rateLimits = new RateLimitService();
|
||||
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
|
||||
services.AddSingleton(rateLimits);
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
var btcPayEnv = provider.GetService<BTCPayServerEnvironment>();
|
||||
var rateLimits = new RateLimitService();
|
||||
if (btcPayEnv.IsDevelopping)
|
||||
{
|
||||
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay");
|
||||
}
|
||||
else
|
||||
{
|
||||
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
|
||||
}
|
||||
return rateLimits;
|
||||
});
|
||||
|
||||
|
||||
services.AddLogging(logBuilder =>
|
||||
@ -275,7 +286,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;
|
||||
|
@ -62,10 +62,12 @@ namespace BTCPayServer.Hosting
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
|
||||
return;
|
||||
}
|
||||
catch (BitpayHttpException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, ex);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -133,13 +135,9 @@ namespace BTCPayServer.Hosting
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = ex.StatusCode;
|
||||
using (var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true))
|
||||
{
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
|
||||
writer.Write(result);
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
|
||||
await httpContext.Response.WriteAsync(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using OpenIddict.Abstractions;
|
||||
@ -49,6 +50,9 @@ namespace BTCPayServer.Hosting
|
||||
Logs.Configure(LoggerFactory);
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
services.AddMemoryCache();
|
||||
services.AddDataProtection()
|
||||
.SetApplicationName("BTCPay Server")
|
||||
.PersistKeysToFileSystem(GetDataDir());
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
@ -140,6 +144,11 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
private DirectoryInfo GetDataDir()
|
||||
{
|
||||
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
|
||||
}
|
||||
|
||||
private void ConfigureOpenIddict(IServiceCollection services)
|
||||
{
|
||||
// Register the OpenIddict services.
|
||||
@ -232,6 +241,10 @@ namespace BTCPayServer.Hosting
|
||||
forwardingOptions.KnownProxies.Clear();
|
||||
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
|
||||
app.UseForwardedHeaders(forwardingOptions);
|
||||
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/Error/Handle", "?statusCode={0}");
|
||||
|
||||
app.UsePayServer();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
@ -243,7 +256,6 @@ namespace BTCPayServer.Hosting
|
||||
app.UseSession();
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
@ -10,21 +10,22 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public class UpdateCrowdfundViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
[Required] [MaxLength(30)] public string Title { get; set; }
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Title { get; set; }
|
||||
|
||||
[MaxLength(50)] public string Tagline { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string Tagline { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Required] public string Description { get; set; }
|
||||
|
||||
[Display(Name = "Featured Image")]
|
||||
public string MainImageUrl { get; set; }
|
||||
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice IPN Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
|
||||
@ -59,7 +60,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
|
||||
|
||||
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
|
||||
[Display(Name = "Reset goal every")]
|
||||
public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
|
||||
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
|
||||
@ -78,7 +80,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string EmbeddedCSS { get; set; }
|
||||
|
||||
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
|
||||
public string AppId { get; set; }
|
||||
public string SearchTerm { get; set; }
|
||||
@ -90,10 +92,16 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
|
||||
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
|
||||
public string Sounds{ get; set; }
|
||||
public string Sounds { get; set; }
|
||||
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
|
||||
public string AnimationColors{ get; set; }
|
||||
public string AnimationColors { get; set; }
|
||||
|
||||
|
||||
// NOTE: Improve validation if needed
|
||||
public bool ModelWithMinimumData
|
||||
{
|
||||
get { return Description != null && Title != null && TargetCurrency != null; }
|
||||
}
|
||||
|
||||
public bool NotificationEmailWarning { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,6 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Display(Name = "Callback Notification Url")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
[Display(Name = "Invoice IPN Notification")]
|
||||
[EmailAddress]
|
||||
public string NotificationEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
@ -84,7 +81,6 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
}
|
||||
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
|
||||
|
||||
public bool NotificationEmailWarning { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
Currency = "USD";
|
||||
}
|
||||
|
||||
[Required]
|
||||
public decimal? Amount
|
||||
{
|
||||
@ -28,11 +29,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
|
||||
[Required]
|
||||
[DisplayName("Store Id")]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[DisplayName("Order Id")]
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
@ -51,18 +54,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
[DisplayName("Buyer Email")]
|
||||
public string BuyerEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[EmailAddress]
|
||||
[DisplayName("Notification Email")]
|
||||
public string NotificationEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Uri]
|
||||
[DisplayName("Notification Url")]
|
||||
public string NotificationUrl
|
||||
@ -72,22 +69,19 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
|
||||
public SelectList Stores
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get; set;
|
||||
}
|
||||
|
||||
[DisplayName("Supported Transaction Currencies")]
|
||||
public List<string> SupportedTransactionCurrencies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get; set;
|
||||
}
|
||||
|
||||
[DisplayName("Available Payment Methods")]
|
||||
public SelectList AvailablePaymentMethods
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
@ -21,6 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string TransactionLink { get; set; }
|
||||
|
||||
public bool Replaced { get; set; }
|
||||
public BitcoinLikePaymentData CryptoPaymentData { get; set; }
|
||||
}
|
||||
|
||||
public class OffChainPaymentViewModel
|
||||
@ -36,7 +38,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Due { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
public string Overpaid { get; set; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -15,26 +15,8 @@ namespace BTCPayServer.Models
|
||||
public StatusSeverity Severity { get; set; }
|
||||
public bool AllowDismiss { get; set; } = true;
|
||||
|
||||
public string SeverityCSS
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Severity)
|
||||
{
|
||||
case StatusSeverity.Info:
|
||||
return "info";
|
||||
case StatusSeverity.Error:
|
||||
return "danger";
|
||||
case StatusSeverity.Success:
|
||||
return "success";
|
||||
case StatusSeverity.Warning:
|
||||
return "warning";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SeverityCSS => ToString(Severity);
|
||||
|
||||
private void ParseNonJsonStatus(string s)
|
||||
{
|
||||
Message = s;
|
||||
@ -43,6 +25,23 @@ namespace BTCPayServer.Models
|
||||
: StatusSeverity.Success;
|
||||
}
|
||||
|
||||
public static string ToString(StatusSeverity severity)
|
||||
{
|
||||
switch (severity)
|
||||
{
|
||||
case StatusSeverity.Info:
|
||||
return "info";
|
||||
case StatusSeverity.Error:
|
||||
return "danger";
|
||||
case StatusSeverity.Success:
|
||||
return "success";
|
||||
case StatusSeverity.Warning:
|
||||
return "warning";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public enum StatusSeverity
|
||||
{
|
||||
Info,
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public WalletId WalletId { get; set; }
|
||||
public bool WalletSupported { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool Collapsed { get; set; }
|
||||
}
|
||||
|
||||
public class AdditionalPaymentMethod
|
||||
|
@ -13,6 +13,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string CryptoCode { get; set; }
|
||||
public string Decoded { get; set; }
|
||||
string _FileName;
|
||||
public bool NBXSeedAvailable { get; set; }
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
|
@ -45,5 +45,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public bool SupportRBF { get; set; }
|
||||
[Display(Name = "Disable RBF")]
|
||||
public bool DisableRBF { get; set; }
|
||||
|
||||
public bool NBXSeedAvailable { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
@ -35,7 +36,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public Task<FeeRate> GetFeeRate;
|
||||
public Task<FeeRate> GetNetworkFeeRate;
|
||||
public Task<BitcoinAddress> ReserveAddress;
|
||||
public Task<KeyPathInformation> ReserveAddress;
|
||||
}
|
||||
|
||||
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
|
||||
@ -141,7 +142,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
onchainMethod.NextNetworkFee = Money.Zero;
|
||||
break;
|
||||
}
|
||||
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
|
||||
onchainMethod.DepositAddress = (await prepare.ReserveAddress).Address.ToString();
|
||||
return onchainMethod;
|
||||
}
|
||||
}
|
||||
|
@ -145,6 +145,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
break;
|
||||
case NBXplorer.Models.NewTransactionEvent evt:
|
||||
wallet.InvalidateCache(evt.DerivationStrategy);
|
||||
_Aggregator.Publish(new NewOnChainTransactionEvent()
|
||||
{
|
||||
CryptoCode = wallet.Network.CryptoCode,
|
||||
NewTransactionEvent = evt
|
||||
});
|
||||
foreach (var output in network.GetValidOutputs(evt))
|
||||
{
|
||||
var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
|
||||
@ -373,7 +378,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address.ToString();
|
||||
btc.DepositAddress = address.Address.ToString();
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, (BTCPayNetwork)network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility);
|
||||
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), (BTCPayNetwork)network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
@ -180,11 +180,15 @@ namespace BTCPayServer.Payments.Lightning
|
||||
model.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC" )
|
||||
{
|
||||
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name);
|
||||
satoshiCulture.NumberFormat.NumberGroupSeparator = " ";
|
||||
|
||||
model.CryptoCode = "Sats";
|
||||
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
|
||||
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
|
||||
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
||||
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
||||
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
|
||||
|
||||
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
|
||||
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
|
||||
|
@ -31,9 +31,14 @@ namespace BTCPayServer.Payments
|
||||
return ((BTCPayNetwork) network).ToString(paymentData);
|
||||
}
|
||||
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(str);
|
||||
return JsonConvert.DeserializeObject<BitcoinLikeOnChainPaymentMethod>(str);
|
||||
}
|
||||
|
||||
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
|
||||
{
|
||||
return JsonConvert.SerializeObject(details);
|
||||
}
|
||||
|
||||
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
|
||||
|
@ -29,11 +29,16 @@ namespace BTCPayServer.Payments
|
||||
return ((BTCPayNetwork) network).ToString(paymentData);
|
||||
}
|
||||
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentMethodDetails>(str);
|
||||
}
|
||||
|
||||
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
|
||||
{
|
||||
return JsonConvert.SerializeObject(details);
|
||||
}
|
||||
|
||||
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<LightningSupportedPaymentMethod>(value.ToString());
|
||||
|
@ -62,9 +62,9 @@ namespace BTCPayServer.Payments
|
||||
public abstract string GetId();
|
||||
public abstract CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str);
|
||||
public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData);
|
||||
public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str);
|
||||
public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str);
|
||||
public abstract string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details);
|
||||
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
|
||||
|
||||
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
|
||||
public abstract string InvoiceViewPaymentPartialName { get; }
|
||||
}
|
||||
|
@ -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,12 +34,13 @@
|
||||
"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",
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
|
@ -76,6 +76,21 @@ namespace BTCPayServer.Security.Bitpay
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevokeLegacyAPIKeys(string storeId)
|
||||
{
|
||||
var keys = await GetLegacyAPIKeys(storeId);
|
||||
if (!keys.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
ctx.ApiKeys.RemoveRange(keys.Select(s => new APIKeyData() {Id = s}));
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
|
@ -24,11 +24,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
return JsonConvert.SerializeObject(paymentData);
|
||||
}
|
||||
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<MoneroLikeOnChainPaymentMethodDetails>(str);
|
||||
}
|
||||
|
||||
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
|
||||
{
|
||||
return JsonConvert.SerializeObject(details);
|
||||
}
|
||||
|
||||
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<MoneroSupportedPaymentMethod>(value.ToString());
|
||||
|
@ -82,8 +82,6 @@ namespace BTCPayServer.Services.Apps
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
|
||||
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
|
||||
};
|
||||
|
||||
public string NotificationEmail { get; set; }
|
||||
}
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
|
@ -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
|
||||
@ -794,7 +792,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
else
|
||||
{
|
||||
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(PaymentMethodDetails.ToString());
|
||||
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString());
|
||||
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||
{
|
||||
btcLike.NextNetworkFee = NextNetworkFee;
|
||||
@ -823,8 +821,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
FeeRate = bitcoinPaymentMethod.FeeRate;
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||
}
|
||||
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
|
||||
PaymentMethodDetails = jobj;
|
||||
PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod));
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return this;
|
||||
@ -849,7 +846,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
int precision = 8;
|
||||
int precision = Network?.Divisibility ?? 8;
|
||||
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
@ -859,8 +856,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
|
||||
paid += _.GetValue(paymentMethods, GetId());
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
|
||||
paid += _.GetValue(paymentMethods, GetId(), null, precision);
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
@ -993,18 +990,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning restore CS0618
|
||||
return this;
|
||||
}
|
||||
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
|
||||
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision)
|
||||
{
|
||||
|
||||
value = value ?? this.GetCryptoPaymentData().GetValue();
|
||||
var to = paymentMethodId;
|
||||
var from = this.GetPaymentMethodId();
|
||||
if (to == from)
|
||||
return decimal.Round(value.Value, 8);
|
||||
return decimal.Round(value.Value, precision);
|
||||
var fromRate = paymentMethods[from].Rate;
|
||||
var toRate = paymentMethods[to].Rate;
|
||||
|
||||
var fiatValue = fromRate * decimal.Round(value.Value, 8);
|
||||
var fiatValue = fromRate * decimal.Round(value.Value, precision);
|
||||
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
|
||||
return otherCurrencyValue;
|
||||
}
|
||||
|
@ -96,8 +96,14 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
{
|
||||
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
|
||||
AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode);
|
||||
}
|
||||
|
||||
_CurrencyProviders.TryAdd("SATS",
|
||||
new NumberFormatInfo()
|
||||
{
|
||||
CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3
|
||||
});
|
||||
}
|
||||
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
|
||||
}
|
||||
@ -180,7 +186,7 @@ namespace BTCPayServer.Services.Rates
|
||||
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||
{
|
||||
Code = network.CryptoCode,
|
||||
Divisibility = 8,
|
||||
Divisibility = network.Divisibility,
|
||||
Name = network.CryptoCode,
|
||||
Crypto = true
|
||||
}))
|
||||
@ -189,6 +195,15 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
dico.TryAdd("SATS", new CurrencyData()
|
||||
{
|
||||
Code = "SATS",
|
||||
Crypto = true,
|
||||
Divisibility = 0,
|
||||
Name = "Satoshis",
|
||||
Symbol = "Sats",
|
||||
});
|
||||
|
||||
return dico.Values.ToArray();
|
||||
}
|
||||
|
||||
|
@ -19,9 +19,9 @@ namespace BTCPayServer.Services
|
||||
_EventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
public async Task<T> GetSettingAsync<T>()
|
||||
public async Task<T> GetSettingAsync<T>(string name = null)
|
||||
{
|
||||
var name = typeof(T).FullName;
|
||||
name ??= typeof(T).FullName;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
|
||||
@ -31,9 +31,9 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateSetting<T>(T obj)
|
||||
public async Task UpdateSetting<T>(T obj, string name = null)
|
||||
{
|
||||
var name = obj.GetType().FullName;
|
||||
name ??= obj.GetType().FullName;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var settings = new SettingData();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user