mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 22:11:48 +01:00
Add Ethereum & ERC20 Support
Add Tests Add Index, XPub to payment data Add Note on local ETH node Fix Sync Summary and Race Condition
This commit is contained in:
parent
7ca0a8c56c
commit
de755ac0bb
38 changed files with 2592 additions and 14 deletions
|
@ -0,0 +1,78 @@
|
|||
#if ALTCOINS
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitEthereum()
|
||||
{
|
||||
Add(new EthereumBTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "ETH",
|
||||
DisplayName = "Ethereum",
|
||||
DefaultRateRules = new[] {"ETH_X = ETH_BTC * BTC_X", "ETH_BTC = kraken(ETH_BTC)"},
|
||||
BlockExplorerLink =
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://etherscan.io/address/{0}"
|
||||
: "https://ropsten.etherscan.io/address/{0}",
|
||||
CryptoImagePath = "/imlegacy/eth.png",
|
||||
ShowSyncSummary = true,
|
||||
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
|
||||
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
|
||||
Divisibility = 18,
|
||||
});
|
||||
}
|
||||
|
||||
public void InitERC20()
|
||||
{
|
||||
if (NetworkType != NetworkType.Mainnet)
|
||||
{
|
||||
Add(new ERC20BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "FAU",
|
||||
DisplayName = "Faucet Token",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FAU_X = FAU_BTC * BTC_X",
|
||||
"FAU_BTC = 0.01",
|
||||
},
|
||||
BlockExplorerLink = "https://ropsten.etherscan.io/address/{0}#tokentxns",
|
||||
ShowSyncSummary = false,
|
||||
CoinType = 1,
|
||||
ChainId = 3,
|
||||
//use https://erc20faucet.com for testnet
|
||||
SmartContractAddress = "0xFab46E002BbF0b4509813474841E0716E6730136",
|
||||
Divisibility = 18,
|
||||
CryptoImagePath = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(new ERC20BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "USDT20",
|
||||
DisplayName = "Tether USD (ERC20)",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"USDT20_UST = 1",
|
||||
"USDT20_X = USDT20_BTC * BTC_X",
|
||||
"USDT20_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
BlockExplorerLink =
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://etherscan.io/address/{0}#tokentxns"
|
||||
: "https://ropsten.etherscan.io/address/{0}#tokentxns",
|
||||
CryptoImagePath = "/imlegacy/liquid-tether.svg",
|
||||
ShowSyncSummary = false,
|
||||
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
|
||||
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
|
||||
SmartContractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
Divisibility = 6
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,20 @@
|
|||
#if ALTCOINS
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class EthereumBTCPayNetwork : BTCPayNetworkBase
|
||||
{
|
||||
public int ChainId { get; set; }
|
||||
public int CoinType { get; set; }
|
||||
|
||||
public string GetDefaultKeyPath()
|
||||
{
|
||||
return $"m/44'/{CoinType}'/0'/0/x";
|
||||
}
|
||||
}
|
||||
|
||||
public class ERC20BTCPayNetwork : EthereumBTCPayNetwork
|
||||
{
|
||||
public string SmartContractAddress { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
20
BTCPayServer.Common/Altcoins/Ethereum/EthereumExtensions.cs
Normal file
20
BTCPayServer.Common/Altcoins/Ethereum/EthereumExtensions.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
#if ALTCOINS
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class EthereumExtensions
|
||||
{
|
||||
|
||||
public static IEnumerable<string> GetAllEthereumSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfiltered)
|
||||
{
|
||||
var ethBased = networkProvider.GetAll().OfType<EthereumBTCPayNetwork>();
|
||||
var chainId = ethBased.Select(network => network.ChainId).Distinct();
|
||||
return unfiltered.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.Where(network => chainId.Contains(network.ChainId))
|
||||
.Select(network => network.CryptoCode.ToUpperInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -6,11 +6,11 @@ namespace BTCPayServer
|
|||
{
|
||||
public static class LiquidExtensions
|
||||
{
|
||||
public static IEnumerable<string> GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfilteredNetworkProvider)
|
||||
public static IEnumerable<string> GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfiltered)
|
||||
{
|
||||
var elementsBased = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>();
|
||||
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
|
||||
return unfilteredNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
|
||||
return unfiltered.GetAll().OfType<ElementsBTCPayNetwork>()
|
||||
.Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,8 @@ namespace BTCPayServer
|
|||
InitPolis();
|
||||
InitChaincoin();
|
||||
InitArgoneum();
|
||||
InitEthereum();
|
||||
InitERC20();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||
|
|
|
@ -1286,5 +1286,26 @@
|
|||
"divisibility":0,
|
||||
"symbol":"Sats",
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name": "Ethereum",
|
||||
"code": "ETH",
|
||||
"divisibility": 18,
|
||||
"symbol": null,
|
||||
"crypto": true
|
||||
},
|
||||
{
|
||||
"name":"USDt",
|
||||
"code":"USDT20",
|
||||
"divisibility":6,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"FaucetToken",
|
||||
"code":"FAU",
|
||||
"divisibility":18,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
}
|
||||
]
|
||||
|
|
110
BTCPayServer.Tests/AltcoinTests/EthereumTests.cs
Normal file
110
BTCPayServer.Tests/AltcoinTests/EthereumTests.cs
Normal file
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Memory;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class EthereumTests
|
||||
{
|
||||
public const int TestTimeout = 60_000;
|
||||
|
||||
public EthereumTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
public void LoadSubChainsAlways()
|
||||
{
|
||||
var options = new BTCPayServerOptions();
|
||||
options.LoadArgs(new ConfigurationRoot(new List<IConfigurationProvider>()
|
||||
{
|
||||
new MemoryConfigurationProvider(new MemoryConfigurationSource()
|
||||
{
|
||||
InitialData = new[] {new KeyValuePair<string, string>("chains", "usdt20"),}
|
||||
})
|
||||
}));
|
||||
|
||||
Assert.NotNull(options.NetworkProvider.GetNetwork("ETH"));
|
||||
Assert.NotNull(options.NetworkProvider.GetNetwork("USDT20"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
public async Task CanUseEthereum()
|
||||
{
|
||||
using var s = SeleniumTester.Create("ETHEREUM", true);
|
||||
s.Server.ActivateETH();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
IWebElement syncSummary = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
syncSummary = s.Driver.FindElement(By.Id("modalDialog"));
|
||||
Assert.True(syncSummary.Displayed);
|
||||
});
|
||||
var web3Link = syncSummary.FindElement(By.LinkText("Configure Web3"));
|
||||
web3Link.Click();
|
||||
s.Driver.FindElement(By.Id("Web3ProviderUrl")).SendKeys("https://ropsten-rpc.linkpool.io");
|
||||
s.Driver.FindElement(By.Id("saveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.AssertElementNotFound(By.Id("modalDialog"));
|
||||
});
|
||||
|
||||
var store = s.CreateNewStore();
|
||||
s.Driver.FindElement(By.LinkText("Ethereum")).Click();
|
||||
|
||||
var seed = new Mnemonic(Wordlist.English);
|
||||
s.Driver.FindElement(By.Id("ModifyETH")).Click();
|
||||
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
s.Driver.FindElement(By.Id("ModifyUSDT20")).Click();
|
||||
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
|
||||
var invoiceId = s.CreateInvoice(store.storeName, 10);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("ETH", currencyDropdownButton.Text);
|
||||
s.Driver.FindElement(By.Id("copy-tab")).Click();
|
||||
|
||||
var ethAddress = s.Driver.FindElements(By.ClassName("copySectionBox"))
|
||||
.Single(element => element.FindElement(By.TagName("label")).Text
|
||||
.Contains("Address", StringComparison.InvariantCultureIgnoreCase)).FindElement(By.TagName("input"))
|
||||
.GetAttribute("value");
|
||||
currencyDropdownButton.Click();
|
||||
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(2, elements.Count);
|
||||
|
||||
elements.Single(element => element.Text.Contains("USDT20")).Click();
|
||||
s.Driver.FindElement(By.Id("copy-tab")).Click();
|
||||
var usdtAddress = s.Driver.FindElements(By.ClassName("copySectionBox"))
|
||||
.Single(element => element.FindElement(By.TagName("label")).Text
|
||||
.Contains("Address", StringComparison.InvariantCultureIgnoreCase)).FindElement(By.TagName("input"))
|
||||
.GetAttribute("value");
|
||||
Assert.Equal(usdtAddress, ethAddress);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -222,6 +222,9 @@ namespace BTCPayServer.Tests
|
|||
var bitpay = new MockRateProvider();
|
||||
bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETB_BTC"), new BidAsk(0.1m)));
|
||||
rateProvider.Providers.Add("bitpay", bitpay);
|
||||
var kraken = new MockRateProvider();
|
||||
kraken.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETH_BTC"), new BidAsk(0.1m)));
|
||||
rateProvider.Providers.Add("kraken", kraken);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -78,6 +78,11 @@ namespace BTCPayServer.Tests
|
|||
PayTester.Chains.Add("LBTC");
|
||||
PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address;
|
||||
}
|
||||
public void ActivateETH()
|
||||
{
|
||||
PayTester.Chains.Add("ETH");
|
||||
}
|
||||
|
||||
#endif
|
||||
public void ActivateLightning()
|
||||
{
|
||||
|
|
|
@ -93,6 +93,7 @@ namespace BTCPayServer.Configuration
|
|||
var filtered = networkProvider.Filter(supportedChains.ToArray());
|
||||
#if ALTCOINS
|
||||
supportedChains.AddRange(filtered.GetAllElementsSubChains(networkProvider));
|
||||
supportedChains.AddRange(filtered.GetAllEthereumSubChains(networkProvider));
|
||||
#endif
|
||||
#if !ALTCOINS
|
||||
var onlyBTC = supportedChains.Count == 1 && supportedChains.First() == "BTC";
|
||||
|
|
|
@ -49,6 +49,7 @@ using NicolasDorier.RateLimits;
|
|||
using Serilog;
|
||||
#if ALTCOINS
|
||||
using BTCPayServer.Services.Altcoins.Monero;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum;
|
||||
#endif
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
|
@ -79,6 +80,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddPayJoinServices();
|
||||
#if ALTCOINS
|
||||
services.AddMoneroLike();
|
||||
services.AddEthereumLike();
|
||||
#endif
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<LabelFactory>();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
#if ALTCOINS
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Monero.Payments;
|
||||
#endif
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -45,6 +46,9 @@ namespace BTCPayServer.Payments
|
|||
case "monerolike":
|
||||
type = PaymentTypes.MoneroLike;
|
||||
break;
|
||||
case "ethereumlike":
|
||||
type = EthereumPaymentType.Instance;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
type = null;
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc,lbtc",
|
||||
"BTCPAY_CHAINS": "btc,ltc,lbtc,eth",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;",
|
||||
"BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#if ALTCOINS
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Configuration
|
||||
{
|
||||
public class EthereumLikeConfiguration
|
||||
{
|
||||
public static string SettingsKey(int chainId)
|
||||
{
|
||||
return $"{nameof(EthereumLikeConfiguration)}_{chainId}";
|
||||
}
|
||||
public int ChainId { get; set; }
|
||||
[Display(Name = "Web3 provider url")]
|
||||
public string Web3ProviderUrl { get; set; }
|
||||
|
||||
[Display(Name = "Web3 provider username (can be left blank)")]
|
||||
public string Web3ProviderUsername { get; set; }
|
||||
|
||||
[Display(Name = "Web3 provider password (can be left blank)")]
|
||||
public string Web3ProviderPassword { get; set; }
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#if ALTCOINS
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using BTCPayServer.Contracts;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum
|
||||
{
|
||||
public static class EthereumLikeExtensions
|
||||
{
|
||||
public const string EthereumInvoiceCheckHttpClient = "EthereumCheck";
|
||||
public const string EthereumInvoiceCreateHttpClient = "EthereumCreate";
|
||||
public static IServiceCollection AddEthereumLike(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<EthereumService>();
|
||||
serviceCollection.AddSingleton<IHostedService, EthereumService>(provider => provider.GetService<EthereumService>());
|
||||
serviceCollection.AddSingleton<EthereumLikePaymentMethodHandler>();
|
||||
serviceCollection.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<EthereumLikePaymentMethodHandler>());
|
||||
serviceCollection.AddSingleton<IStoreNavExtension, EthereumStoreNavExtension>();
|
||||
serviceCollection.AddTransient<NoRedirectHttpClientHandler>();
|
||||
serviceCollection.AddSingleton<ISyncSummaryProvider, EthereumSyncSummaryProvider>();
|
||||
serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<NoRedirectHttpClientHandler>();
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
||||
|
||||
public class NoRedirectHttpClientHandler : HttpClientHandler
|
||||
{
|
||||
public NoRedirectHttpClientHandler()
|
||||
{
|
||||
this.AllowAutoRedirect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,11 @@
|
|||
#if ALTCOINS
|
||||
using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum
|
||||
{
|
||||
public class EthereumStoreNavExtension: IStoreNavExtension
|
||||
{
|
||||
public string Partial { get; } = "Ethereum/StoreNavEthereumExtension";
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,26 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Filters
|
||||
{
|
||||
public class OnlyIfSupportEthAttribute : Attribute, IAsyncActionFilter
|
||||
{
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var options = (BTCPayServerOptions) context.HttpContext.RequestServices.GetService(typeof(BTCPayServerOptions));
|
||||
if (!options.NetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>().Any())
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,37 @@
|
|||
#if ALTCOINS
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
{
|
||||
public class EthereumLikeOnChainPaymentMethodDetails : IPaymentMethodDetails
|
||||
{
|
||||
public PaymentType GetPaymentType()
|
||||
{
|
||||
return EthereumPaymentType.Instance;
|
||||
}
|
||||
|
||||
public string GetPaymentDestination()
|
||||
{
|
||||
return DepositAddress;
|
||||
}
|
||||
|
||||
public decimal GetNextNetworkFee()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public decimal GetFeeRate()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void SetPaymentDestination(string newPaymentDestination)
|
||||
{
|
||||
DepositAddress = newPaymentDestination;
|
||||
}
|
||||
public long Index { get; set; }
|
||||
public string XPub { get; set; }
|
||||
public string DepositAddress { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,77 @@
|
|||
#if ALTCOINS
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using Nethereum.Web3;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
{
|
||||
public class EthereumLikePaymentData : CryptoPaymentData
|
||||
{
|
||||
public long Amount { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
public long AccountIndex { get; set; }
|
||||
public string XPub { get; set; }
|
||||
public long ConfirmationCount { get; set; }
|
||||
public BTCPayNetworkBase Network { get; set; }
|
||||
public long? BlockNumber { get; set; }
|
||||
|
||||
public string GetPaymentId()
|
||||
{
|
||||
return GetPaymentId(CryptoCode,Address, Amount);
|
||||
}
|
||||
|
||||
public static string GetPaymentId(string cryptoCode, string address, long amount)
|
||||
{
|
||||
return $"{cryptoCode}#{address}#{amount}";
|
||||
}
|
||||
|
||||
public string[] GetSearchTerms()
|
||||
{
|
||||
return new[] {Address};
|
||||
}
|
||||
|
||||
public decimal GetValue()
|
||||
{
|
||||
return decimal.Parse(Web3.Convert.FromWeiToBigDecimal(Amount, Network.Divisibility).ToString(),
|
||||
CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public bool PaymentCompleted(PaymentEntity entity)
|
||||
{
|
||||
return ConfirmationCount >= 25;
|
||||
}
|
||||
|
||||
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy)
|
||||
{
|
||||
switch (speedPolicy)
|
||||
{
|
||||
case SpeedPolicy.HighSpeed:
|
||||
return ConfirmationCount >= 2;
|
||||
case SpeedPolicy.MediumSpeed:
|
||||
return ConfirmationCount >= 6;
|
||||
case SpeedPolicy.LowMediumSpeed:
|
||||
return ConfirmationCount >= 12;
|
||||
case SpeedPolicy.LowSpeed:
|
||||
return ConfirmationCount >= 20;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public PaymentType GetPaymentType()
|
||||
{
|
||||
return EthereumPaymentType.Instance;
|
||||
}
|
||||
|
||||
public string GetDestination()
|
||||
{
|
||||
return Address;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,134 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
{
|
||||
public class
|
||||
EthereumLikePaymentMethodHandler : PaymentMethodHandlerBase<EthereumSupportedPaymentMethod,
|
||||
EthereumBTCPayNetwork>
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly EthereumService _ethereumService;
|
||||
|
||||
public EthereumLikePaymentMethodHandler(BTCPayNetworkProvider networkProvider, EthereumService ethereumService)
|
||||
{
|
||||
_networkProvider = networkProvider;
|
||||
_ethereumService = ethereumService;
|
||||
}
|
||||
|
||||
public override PaymentType PaymentType => EthereumPaymentType.Instance;
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(InvoiceLogs logs,
|
||||
EthereumSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod,
|
||||
StoreData store, EthereumBTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
if (!_ethereumService.IsAvailable(network.CryptoCode, out var error))
|
||||
throw new PaymentMethodUnavailableException(error??$"Not configured yet");
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
if (!(preparePaymentObject is Prepare ethPrepare)) throw new ArgumentException();
|
||||
var address = await ethPrepare.ReserveAddress(invoice.Id);
|
||||
if (address is null || address.Failed)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"could not generate address");
|
||||
}
|
||||
|
||||
return new EthereumLikeOnChainPaymentMethodDetails()
|
||||
{
|
||||
DepositAddress = address.Address, Index = address.Index, XPub = address.XPub
|
||||
};
|
||||
}
|
||||
|
||||
public override object PreparePayment(EthereumSupportedPaymentMethod supportedPaymentMethod, StoreData store,
|
||||
BTCPayNetworkBase network)
|
||||
{
|
||||
return new Prepare()
|
||||
{
|
||||
ReserveAddress = s =>
|
||||
_ethereumService.ReserveNextAddress(
|
||||
new EthereumService.ReserveEthereumAddress()
|
||||
{
|
||||
StoreId = store.Id, CryptoCode = network.CryptoCode
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
class Prepare
|
||||
{
|
||||
public Func<string, Task<EthereumService.ReserveEthereumAddressResponse>> ReserveAddress;
|
||||
}
|
||||
|
||||
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
|
||||
StoreBlob storeBlob)
|
||||
{
|
||||
var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentType);
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var network = _networkProvider.GetNetwork<EthereumBTCPayNetwork>(model.CryptoCode);
|
||||
model.IsLightning = false;
|
||||
model.PaymentMethodName = GetPaymentMethodName(network);
|
||||
model.CryptoImage = GetCryptoImage(network);
|
||||
model.InvoiceBitcoinUrl = "";
|
||||
model.InvoiceBitcoinUrlQR = cryptoInfo.Address;
|
||||
}
|
||||
|
||||
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<EthereumBTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||
return GetCryptoImage(network);
|
||||
}
|
||||
|
||||
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<EthereumBTCPayNetwork>(paymentMethodId.CryptoCode);
|
||||
return GetPaymentMethodName(network);
|
||||
}
|
||||
|
||||
public override Task<string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob,
|
||||
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount,
|
||||
PaymentMethodId paymentMethodId)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
|
||||
{
|
||||
return _networkProvider.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.Select(network => new PaymentMethodId(network.CryptoCode, PaymentType));
|
||||
}
|
||||
|
||||
public override CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
|
||||
{
|
||||
return new CheckoutUIPaymentMethodSettings()
|
||||
{
|
||||
ExtensionPartial = "Ethereum/EthereumLikeMethodCheckout",
|
||||
CheckoutBodyVueComponentName = "EthereumLikeMethodCheckout",
|
||||
CheckoutHeaderVueComponentName = "EthereumLikeMethodCheckoutHeader",
|
||||
NoScriptPartialName = "Bitcoin_Lightning_LikeMethodCheckoutNoScript"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetCryptoImage(EthereumBTCPayNetwork network)
|
||||
{
|
||||
return network.CryptoImagePath;
|
||||
}
|
||||
|
||||
|
||||
private string GetPaymentMethodName(EthereumBTCPayNetwork network)
|
||||
{
|
||||
return $"{network.DisplayName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,59 @@
|
|||
#if ALTCOINS
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Monero.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
{
|
||||
public class EthereumPaymentType: PaymentType
|
||||
{
|
||||
public static EthereumPaymentType Instance { get; } = new EthereumPaymentType();
|
||||
public override string ToPrettyString() => "On-Chain";
|
||||
|
||||
public override string GetId()=> "EthereumLike";
|
||||
|
||||
|
||||
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<EthereumLikePaymentData>(str);
|
||||
}
|
||||
|
||||
public override string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData)
|
||||
{
|
||||
return JsonConvert.SerializeObject(paymentData);
|
||||
}
|
||||
|
||||
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<EthereumLikeOnChainPaymentMethodDetails>(str);
|
||||
}
|
||||
|
||||
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
|
||||
{
|
||||
return JsonConvert.SerializeObject(details);
|
||||
}
|
||||
|
||||
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<EthereumSupportedPaymentMethod>(value.ToString());
|
||||
}
|
||||
|
||||
public override string GetTransactionLink(BTCPayNetworkBase network, string txId)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
}
|
||||
|
||||
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue,
|
||||
string serverUri)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
public override string InvoiceViewPaymentPartialName { get; }= "Ethereum/ViewEthereumLikePaymentData";
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,41 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using Nethereum.HdWallet;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
{
|
||||
public class EthereumSupportedPaymentMethod : ISupportedPaymentMethod
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string XPub { get; set; }
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, EthereumPaymentType.Instance);
|
||||
public long CurrentIndex { get; set; }
|
||||
public string KeyPath { get; set; }
|
||||
|
||||
public Func<int, string> GetWalletDerivator()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(XPub))
|
||||
{
|
||||
try
|
||||
{
|
||||
return new PublicWallet(XPub).GetAddress;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new PublicWallet(new BitcoinExtPubKey(XPub, Network.Main).ExtPubKey).GetAddress;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(XPub))
|
||||
{
|
||||
return i => new Wallet(Seed, Password, KeyPath).GetAccount(i).Address;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,313 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Payments;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Configuration;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.UI;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Services
|
||||
{
|
||||
public class EthereumService : EventHostedServiceBase
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly Dictionary<int, EthereumWatcher> _chainHostedServices = new Dictionary<int, EthereumWatcher>();
|
||||
|
||||
private readonly Dictionary<int, CancellationTokenSource> _chainHostedServiceCancellationTokenSources =
|
||||
new Dictionary<int, CancellationTokenSource>();
|
||||
public EthereumService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
SettingsRepository settingsRepository,
|
||||
InvoiceRepository invoiceRepository,
|
||||
IConfiguration configuration) : base(
|
||||
eventAggregator)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_settingsRepository = settingsRepository;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var chainIds = _btcPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.Select(network => network.ChainId).Distinct().ToList();
|
||||
if (!chainIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await base.StartAsync(cancellationToken);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_eventAggregator.Publish(new CheckWatchers());
|
||||
await Task.Delay(IsAllAvailable()? TimeSpan.FromDays(1): TimeSpan.FromSeconds(5) , cancellationToken);
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
private static bool First = true;
|
||||
private async Task LoopThroughChainWatchers(CancellationToken cancellationToken)
|
||||
{
|
||||
var chainIds = _btcPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.Select(network => network.ChainId).Distinct().ToList();
|
||||
foreach (var chainId in chainIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = await _settingsRepository.GetSettingAsync<EthereumLikeConfiguration>(
|
||||
EthereumLikeConfiguration.SettingsKey(chainId));
|
||||
if (settings is null || string.IsNullOrEmpty(settings.Web3ProviderUrl))
|
||||
{
|
||||
var val = _configuration.GetValue<string>($"chain{chainId}_web3", null);
|
||||
var valUser = _configuration.GetValue<string>($"chain{chainId}_web3_user", null);
|
||||
var valPass = _configuration.GetValue<string>($"chain{chainId}_web3_password", null);
|
||||
if (val != null && First)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Setting eth chain {chainId} web3 to {val}");
|
||||
settings ??= new EthereumLikeConfiguration()
|
||||
{
|
||||
ChainId = chainId,
|
||||
Web3ProviderUrl = val,
|
||||
Web3ProviderPassword = valPass,
|
||||
Web3ProviderUsername = valUser
|
||||
};
|
||||
await _settingsRepository.UpdateSetting(settings, EthereumLikeConfiguration.SettingsKey(chainId));
|
||||
}
|
||||
}
|
||||
var currentlyRunning = _chainHostedServices.ContainsKey(chainId);
|
||||
var valid = await EthereumConfigController.CheckValid(_httpClientFactory, _btcPayNetworkProvider.NetworkType, settings?.InvoiceId);
|
||||
if (!currentlyRunning || (currentlyRunning && !valid))
|
||||
{
|
||||
await HandleChainWatcher(settings, valid, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
First = false;
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var chainHostedService in _chainHostedServices.Values)
|
||||
{
|
||||
_ = chainHostedService.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
base.SubscribeToEvents();
|
||||
|
||||
Subscribe<ReserveEthereumAddress>();
|
||||
Subscribe<SettingsChanged<EthereumLikeConfiguration>>();
|
||||
Subscribe<CheckWatchers>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is ReserveEthereumAddress reserveEthereumAddress)
|
||||
{
|
||||
await HandleReserveNextAddress(reserveEthereumAddress);
|
||||
}
|
||||
|
||||
if (evt is SettingsChanged<EthereumLikeConfiguration> settingsChangedEthConfig)
|
||||
{
|
||||
var valid = await EthereumConfigController.CheckValid(_httpClientFactory, _btcPayNetworkProvider.NetworkType, settingsChangedEthConfig?.Settings?.InvoiceId);
|
||||
await HandleChainWatcher(settingsChangedEthConfig.Settings, valid, cancellationToken);
|
||||
}
|
||||
|
||||
if (evt is CheckWatchers)
|
||||
{
|
||||
await LoopThroughChainWatchers(cancellationToken);
|
||||
}
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task HandleChainWatcher(EthereumLikeConfiguration ethereumLikeConfiguration, bool valid,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (ethereumLikeConfiguration is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chainHostedServiceCancellationTokenSources.ContainsKey(ethereumLikeConfiguration.ChainId))
|
||||
{
|
||||
_chainHostedServiceCancellationTokenSources[ethereumLikeConfiguration.ChainId].Cancel();
|
||||
_chainHostedServiceCancellationTokenSources.Remove(ethereumLikeConfiguration.ChainId);
|
||||
}
|
||||
|
||||
if (_chainHostedServices.ContainsKey(ethereumLikeConfiguration.ChainId))
|
||||
{
|
||||
await _chainHostedServices[ethereumLikeConfiguration.ChainId].StopAsync(cancellationToken);
|
||||
_chainHostedServices.Remove(ethereumLikeConfiguration.ChainId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ethereumLikeConfiguration.Web3ProviderUrl) && valid)
|
||||
{
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
_chainHostedServiceCancellationTokenSources.AddOrReplace(ethereumLikeConfiguration.ChainId, cts);
|
||||
_chainHostedServices.AddOrReplace(ethereumLikeConfiguration.ChainId,
|
||||
new EthereumWatcher(ethereumLikeConfiguration.ChainId, ethereumLikeConfiguration,
|
||||
_btcPayNetworkProvider, _eventAggregator, _invoiceRepository));
|
||||
await _chainHostedServices[ethereumLikeConfiguration.ChainId].StartAsync(CancellationTokenSource
|
||||
.CreateLinkedTokenSource(cancellationToken, cts.Token).Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleReserveNextAddress(ReserveEthereumAddress reserveEthereumAddress)
|
||||
{
|
||||
var store = await _storeRepository.FindStore(reserveEthereumAddress.StoreId);
|
||||
var ethereumSupportedPaymentMethod = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<EthereumSupportedPaymentMethod>()
|
||||
.SingleOrDefault(method => method.PaymentId.CryptoCode == reserveEthereumAddress.CryptoCode);
|
||||
if (ethereumSupportedPaymentMethod == null)
|
||||
{
|
||||
_eventAggregator.Publish(new ReserveEthereumAddressResponse()
|
||||
{
|
||||
OpId = reserveEthereumAddress.OpId, Failed = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ethereumSupportedPaymentMethod.CurrentIndex++;
|
||||
var address = ethereumSupportedPaymentMethod.GetWalletDerivator()?
|
||||
.Invoke((int)ethereumSupportedPaymentMethod.CurrentIndex);
|
||||
|
||||
if (string.IsNullOrEmpty(address))
|
||||
{
|
||||
_eventAggregator.Publish(new ReserveEthereumAddressResponse()
|
||||
{
|
||||
OpId = reserveEthereumAddress.OpId, Failed = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
store.SetSupportedPaymentMethod(ethereumSupportedPaymentMethod.PaymentId,
|
||||
ethereumSupportedPaymentMethod);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
_eventAggregator.Publish(new ReserveEthereumAddressResponse()
|
||||
{
|
||||
Address = address,
|
||||
Index = ethereumSupportedPaymentMethod.CurrentIndex,
|
||||
CryptoCode = ethereumSupportedPaymentMethod.CryptoCode,
|
||||
OpId = reserveEthereumAddress.OpId,
|
||||
StoreId = reserveEthereumAddress.StoreId,
|
||||
XPub = ethereumSupportedPaymentMethod.XPub
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ReserveEthereumAddressResponse> ReserveNextAddress(ReserveEthereumAddress address)
|
||||
{
|
||||
address.OpId = string.IsNullOrEmpty(address.OpId) ? Guid.NewGuid().ToString() : address.OpId;
|
||||
var tcs = new TaskCompletionSource<ReserveEthereumAddressResponse>();
|
||||
var subscription = _eventAggregator.Subscribe<ReserveEthereumAddressResponse>(response =>
|
||||
{
|
||||
if (response.OpId == address.OpId)
|
||||
{
|
||||
tcs.SetResult(response);
|
||||
}
|
||||
});
|
||||
_eventAggregator.Publish(address);
|
||||
|
||||
if (tcs.Task.Wait(TimeSpan.FromSeconds(60)))
|
||||
{
|
||||
subscription?.Dispose();
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
subscription?.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
public class CheckWatchers
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public class ReserveEthereumAddressResponse
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
public long Index { get; set; }
|
||||
public string OpId { get; set; }
|
||||
public string XPub { get; set; }
|
||||
public bool Failed { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Reserved {CryptoCode} address {Address} for store {StoreId}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ReserveEthereumAddress
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string OpId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Reserving {CryptoCode} address for store {StoreId}";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAllAvailable()
|
||||
{
|
||||
return _btcPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.All(network => IsAvailable(network.CryptoCode, out _));
|
||||
}
|
||||
|
||||
public bool IsAvailable(string networkCryptoCode, out string error)
|
||||
{
|
||||
error = null;
|
||||
var chainId = _btcPayNetworkProvider.GetNetwork<EthereumBTCPayNetwork>(networkCryptoCode)?.ChainId;
|
||||
if (chainId != null && _chainHostedServices.TryGetValue(chainId.Value, out var watcher))
|
||||
{
|
||||
error = watcher.GlobalError;
|
||||
return string.IsNullOrEmpty(watcher.GlobalError);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,23 @@
|
|||
#if ALTCOINS
|
||||
using BTCPayServer.Contracts;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Services
|
||||
{
|
||||
public class EthereumSyncSummaryProvider : ISyncSummaryProvider
|
||||
{
|
||||
private readonly EthereumService _ethereumService;
|
||||
|
||||
public EthereumSyncSummaryProvider(EthereumService ethereumService)
|
||||
{
|
||||
_ethereumService = ethereumService;
|
||||
}
|
||||
|
||||
public bool AllAvailable()
|
||||
{
|
||||
return _ethereumService.IsAllAvailable();
|
||||
}
|
||||
|
||||
public string Partial { get; } = "Ethereum/ETHSyncSummary";
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,378 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Configuration;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.StandardTokenEIP20.ContractDefinition;
|
||||
using Nethereum.Web3;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.Services
|
||||
{
|
||||
public class EthereumWatcher : EventHostedServiceBase
|
||||
{
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private int ChainId { get; }
|
||||
private readonly HashSet<PaymentMethodId> PaymentMethods;
|
||||
|
||||
private readonly Web3 Web3;
|
||||
private readonly List<EthereumBTCPayNetwork> Networks;
|
||||
public string GlobalError { get; private set; } = "The chain watcher is still starting.";
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.NodeServer.LogInformation($"Starting EthereumWatcher for chain {ChainId}");
|
||||
var result = await Web3.Eth.ChainId.SendRequestAsync();
|
||||
if (result.Value != ChainId)
|
||||
{
|
||||
GlobalError =
|
||||
$"The web3 client is connected to a different chain id. Expected {ChainId} but Web3 returned {result.Value}";
|
||||
return;
|
||||
}
|
||||
|
||||
await base.StartAsync(cancellationToken);
|
||||
_eventAggregator.Publish(new CatchUp());
|
||||
GlobalError = null;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<EthereumService.ReserveEthereumAddressResponse>();
|
||||
Subscribe<EthereumAddressBalanceFetched>();
|
||||
Subscribe<CatchUp>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is CatchUp)
|
||||
{
|
||||
DateTimeOffset start = DateTimeOffset.Now;
|
||||
await UpdateAnyPendingEthLikePaymentAndAddressWatchList(cancellationToken);
|
||||
|
||||
TimeSpan diff = start - DateTimeOffset.Now;
|
||||
if (diff.TotalSeconds < 5)
|
||||
{
|
||||
_ = Task.Delay(TimeSpan.FromSeconds(5 - diff.TotalSeconds), cancellationToken).ContinueWith(task =>
|
||||
{
|
||||
_eventAggregator.Publish(new CatchUp());
|
||||
return Task.CompletedTask;
|
||||
}, cancellationToken, TaskContinuationOptions.None, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
if (evt is EthereumAddressBalanceFetched response)
|
||||
{
|
||||
if (response.ChainId != ChainId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var network = Networks.SingleOrDefault(payNetwork =>
|
||||
payNetwork.CryptoCode.Equals(response.CryptoCode, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (network is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var invoice = response.InvoiceEntity;
|
||||
if (invoice is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingPayment = response.MatchedExistingPayment;
|
||||
|
||||
if (existingPayment is null && response.Amount > 0)
|
||||
{
|
||||
//new payment
|
||||
var paymentData = new EthereumLikePaymentData()
|
||||
{
|
||||
Address = response.Address,
|
||||
CryptoCode = response.CryptoCode,
|
||||
Amount = response.Amount,
|
||||
Network = network,
|
||||
BlockNumber =
|
||||
response.BlockParameter.ParameterType == BlockParameter.BlockParameterType.blockNumber
|
||||
? (long?)response.BlockParameter.BlockNumber.Value
|
||||
: (long?)null,
|
||||
ConfirmationCount = 0,
|
||||
AccountIndex = response.PaymentMethodDetails.Index,
|
||||
XPub = response.PaymentMethodDetails.XPub
|
||||
};
|
||||
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow,
|
||||
paymentData, network, true);
|
||||
if (payment != null) ReceivedPayment(invoice, payment);
|
||||
}
|
||||
else if (existingPayment != null)
|
||||
{
|
||||
var cd = (EthereumLikePaymentData)existingPayment.GetCryptoPaymentData();
|
||||
//existing payment amount was changed. Set to unaccounted and register as a new payment.
|
||||
if (response.Amount == 0 || response.Amount != cd.Amount)
|
||||
{
|
||||
existingPayment.Accounted = false;
|
||||
|
||||
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment});
|
||||
if (response.Amount > 0)
|
||||
{
|
||||
var paymentData = new EthereumLikePaymentData()
|
||||
{
|
||||
Address = response.Address,
|
||||
CryptoCode = response.CryptoCode,
|
||||
Amount = response.Amount,
|
||||
Network = network,
|
||||
BlockNumber =
|
||||
response.BlockParameter.ParameterType ==
|
||||
BlockParameter.BlockParameterType.blockNumber
|
||||
? (long?)response.BlockParameter.BlockNumber.Value
|
||||
: null,
|
||||
ConfirmationCount =
|
||||
response.BlockParameter.ParameterType ==
|
||||
BlockParameter.BlockParameterType.blockNumber
|
||||
? 1
|
||||
: 0,
|
||||
|
||||
AccountIndex = cd.AccountIndex,
|
||||
XPub = cd.XPub
|
||||
};
|
||||
var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow,
|
||||
paymentData, network, true);
|
||||
if (payment != null) ReceivedPayment(invoice, payment);
|
||||
}
|
||||
}
|
||||
else if (response.Amount == cd.Amount)
|
||||
{
|
||||
//transition from pending to 1 confirmed
|
||||
if (cd.BlockNumber is null && response.BlockParameter.ParameterType ==
|
||||
BlockParameter.BlockParameterType.blockNumber)
|
||||
{
|
||||
cd.ConfirmationCount = 1;
|
||||
cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value;
|
||||
|
||||
existingPayment.SetCryptoPaymentData(cd);
|
||||
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment});
|
||||
|
||||
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
|
||||
}
|
||||
//increment confirm count
|
||||
else if (response.BlockParameter.ParameterType ==
|
||||
BlockParameter.BlockParameterType.blockNumber)
|
||||
{
|
||||
if (response.BlockParameter.BlockNumber.Value > cd.BlockNumber.Value)
|
||||
{
|
||||
cd.ConfirmationCount =
|
||||
(long)(response.BlockParameter.BlockNumber.Value - cd.BlockNumber.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
cd.BlockNumber = (long?)response.BlockParameter.BlockNumber.Value;
|
||||
cd.ConfirmationCount = 1;
|
||||
}
|
||||
|
||||
existingPayment.SetCryptoPaymentData(cd);
|
||||
await _invoiceRepository.UpdatePayments(new List<PaymentEntity>() {existingPayment});
|
||||
|
||||
_eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CatchUp
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.NodeServer.LogInformation($"Stopping EthereumWatcher for chain {ChainId}");
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
private async Task UpdateAnyPendingEthLikePaymentAndAddressWatchList(CancellationToken cancellationToken)
|
||||
{
|
||||
var invoiceIds = await _invoiceRepository.GetPendingInvoices();
|
||||
if (!invoiceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery() {InvoiceId = invoiceIds});
|
||||
invoices = invoices
|
||||
.Where(entity => PaymentMethods.Any(id => entity.GetPaymentMethod(id) != null))
|
||||
.ToArray();
|
||||
|
||||
await UpdatePaymentStates(invoices, cancellationToken);
|
||||
}
|
||||
|
||||
private long? LastBlock = null;
|
||||
|
||||
private async Task UpdatePaymentStates(InvoiceEntity[] invoices, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!invoices.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentBlock = await Web3.Eth.Blocks.GetBlockNumber.SendRequestAsync();
|
||||
|
||||
foreach (var network in Networks)
|
||||
{
|
||||
var erc20Network = network as ERC20BTCPayNetwork;
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, EthereumPaymentType.Instance);
|
||||
var expandedInvoices = invoices
|
||||
.Select(entity => (
|
||||
Invoice: entity,
|
||||
PaymentMethodDetails: entity.GetPaymentMethods().TryGet(paymentMethodId),
|
||||
ExistingPayments: entity.GetPayments(network).Select(paymentEntity => (Payment: paymentEntity,
|
||||
PaymentData: (EthereumLikePaymentData)paymentEntity.GetCryptoPaymentData(),
|
||||
Invoice: entity))
|
||||
)).Where(tuple => tuple.PaymentMethodDetails != null).ToList();
|
||||
|
||||
var existingPaymentData = expandedInvoices.SelectMany(tuple =>
|
||||
tuple.ExistingPayments.Where(valueTuple => valueTuple.Payment.Accounted)).ToList();
|
||||
|
||||
var noAccountedPaymentInvoices = expandedInvoices.Where(tuple =>
|
||||
tuple.ExistingPayments.All(valueTuple => !valueTuple.Payment.Accounted)).ToList();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
if (existingPaymentData.Any() && currentBlock.Value != LastBlock)
|
||||
{
|
||||
Logs.NodeServer.LogInformation(
|
||||
$"Checking {existingPaymentData.Count} existing payments on {expandedInvoices.Count} invoices on {network.CryptoCode}");
|
||||
var blockParameter = new BlockParameter(currentBlock);
|
||||
|
||||
tasks.Add(Task.WhenAll(existingPaymentData.Select(async tuple =>
|
||||
{
|
||||
var bal = await GetBalance(network, blockParameter, tuple.PaymentData.Address);
|
||||
_eventAggregator.Publish(new EthereumAddressBalanceFetched()
|
||||
{
|
||||
Address = tuple.PaymentData.Address,
|
||||
CryptoCode = network.CryptoCode,
|
||||
Amount = bal,
|
||||
MatchedExistingPayment = tuple.Payment,
|
||||
BlockParameter = blockParameter,
|
||||
ChainId = ChainId,
|
||||
InvoiceEntity = tuple.Invoice,
|
||||
});
|
||||
})).ContinueWith(task =>
|
||||
{
|
||||
LastBlock = (long?)currentBlock.Value;
|
||||
}, TaskScheduler.Current));
|
||||
}
|
||||
|
||||
if (noAccountedPaymentInvoices.Any())
|
||||
{
|
||||
Logs.NodeServer.LogInformation(
|
||||
$"Checking {noAccountedPaymentInvoices.Count} addresses for new payments on {network.CryptoCode}");
|
||||
var blockParameter = BlockParameter.CreatePending();
|
||||
tasks.AddRange(noAccountedPaymentInvoices.Select(async tuple =>
|
||||
{
|
||||
var bal = await GetBalance(network, blockParameter,
|
||||
tuple.PaymentMethodDetails.GetPaymentMethodDetails().GetPaymentDestination());
|
||||
_eventAggregator.Publish(new EthereumAddressBalanceFetched()
|
||||
{
|
||||
Address = tuple.PaymentMethodDetails.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
CryptoCode = network.CryptoCode,
|
||||
Amount = bal,
|
||||
MatchedExistingPayment = null,
|
||||
BlockParameter = blockParameter,
|
||||
ChainId = ChainId,
|
||||
InvoiceEntity = tuple.Invoice,
|
||||
PaymentMethodDetails = (EthereumLikeOnChainPaymentMethodDetails) tuple.PaymentMethodDetails.GetPaymentMethodDetails()
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
public class EthereumAddressBalanceFetched
|
||||
{
|
||||
public BlockParameter BlockParameter { get; set; }
|
||||
public int ChainId { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public InvoiceEntity InvoiceEntity { get; set; }
|
||||
public PaymentEntity MatchedExistingPayment { get; set; }
|
||||
public EthereumLikeOnChainPaymentMethodDetails PaymentMethodDetails { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
|
||||
{
|
||||
_eventAggregator.Publish(
|
||||
new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) {Payment = payment});
|
||||
}
|
||||
|
||||
private async Task<long> GetBalance(EthereumBTCPayNetwork network, BlockParameter blockParameter,
|
||||
string address)
|
||||
{
|
||||
if (network is ERC20BTCPayNetwork erc20BTCPayNetwork)
|
||||
{
|
||||
return (long)(await Web3.Eth.GetContractHandler(erc20BTCPayNetwork.SmartContractAddress)
|
||||
.QueryAsync<BalanceOfFunction, BigInteger>(new BalanceOfFunction() {Owner = address}));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (long)(await Web3.Eth.GetBalance.SendRequestAsync(address, blockParameter)).Value;
|
||||
}
|
||||
}
|
||||
|
||||
public EthereumWatcher(int chainId, EthereumLikeConfiguration config,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
EventAggregator eventAggregator, InvoiceRepository invoiceRepository) :
|
||||
base(eventAggregator)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
ChainId = chainId;
|
||||
AuthenticationHeaderValue headerValue = null;
|
||||
if (!string.IsNullOrEmpty(config.Web3ProviderUsername))
|
||||
{
|
||||
var val = config.Web3ProviderUsername;
|
||||
if (!string.IsNullOrEmpty(config.Web3ProviderUsername))
|
||||
{
|
||||
val += $":{config.Web3ProviderUsername}";
|
||||
}
|
||||
|
||||
headerValue = new AuthenticationHeaderValue(
|
||||
"Basic", Convert.ToBase64String(
|
||||
System.Text.Encoding.ASCII.GetBytes(val)));
|
||||
}
|
||||
Web3 = new Web3(config.Web3ProviderUrl, null, headerValue);
|
||||
Networks = btcPayNetworkProvider.GetAll()
|
||||
.OfType<EthereumBTCPayNetwork>()
|
||||
.Where(network => network.ChainId == chainId)
|
||||
.ToList();
|
||||
PaymentMethods = Networks
|
||||
.Select(network => new PaymentMethodId(network.CryptoCode, EthereumPaymentType.Instance))
|
||||
.ToHashSet();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,169 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Configuration;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Filters;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using Nethereum.Hex.HexConvertors.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
{
|
||||
[Route("ethconfig")]
|
||||
[OnlyIfSupportEth]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class EthereumConfigController : Controller
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public EthereumConfigController(IHttpClientFactory httpClientFactory, SettingsRepository settingsRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_settingsRepository = settingsRepository;
|
||||
_userManager = userManager;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
[HttpGet("{chainId}")]
|
||||
public async Task<IActionResult> UpdateChainConfig(int chainId)
|
||||
{
|
||||
return View("Ethereum/UpdateChainConfig",
|
||||
(await _settingsRepository.GetSettingAsync<EthereumLikeConfiguration>(
|
||||
EthereumLikeConfiguration.SettingsKey(chainId))) ?? new EthereumLikeConfiguration()
|
||||
{
|
||||
ChainId = chainId, Web3ProviderUrl = ""
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{chainId}/cb")]
|
||||
public IActionResult Callback(int chainId)
|
||||
{
|
||||
_eventAggregator.Publish(new EthereumService.CheckWatchers());
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "If the invoice was paid successfully and confirmed, the system will be enabled momentarily"
|
||||
});
|
||||
return RedirectToAction("UpdateChainConfig", new {chainId});
|
||||
}
|
||||
|
||||
[HttpPost("{chainId}")]
|
||||
public async Task<IActionResult> UpdateChainConfig(int chainId, EthereumLikeConfiguration vm)
|
||||
{
|
||||
var current = await _settingsRepository.GetSettingAsync<EthereumLikeConfiguration>(
|
||||
EthereumLikeConfiguration.SettingsKey(chainId));
|
||||
if (current?.Web3ProviderUrl != vm.Web3ProviderUrl || current?.InvoiceId != vm.InvoiceId)
|
||||
{
|
||||
vm.ChainId = chainId;
|
||||
await _settingsRepository.UpdateSetting(vm, EthereumLikeConfiguration.SettingsKey(chainId));
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success, Message = $"Chain {chainId} updated"
|
||||
});
|
||||
return RedirectToAction(nameof(UpdateChainConfig));
|
||||
}
|
||||
|
||||
[HttpGet("{chainId}/p")]
|
||||
[HttpPost("{chainId}/p")]
|
||||
public async Task<IActionResult> CreateInvoice(int chainId)
|
||||
{
|
||||
var current = await _settingsRepository.GetSettingAsync<EthereumLikeConfiguration>(
|
||||
EthereumLikeConfiguration.SettingsKey(chainId));
|
||||
current ??= new EthereumLikeConfiguration() {ChainId = chainId};
|
||||
if (!string.IsNullOrEmpty(current?.InvoiceId) &&
|
||||
Request.Method.Equals("get", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return View("Confirm",
|
||||
new ConfirmModel()
|
||||
{
|
||||
Title = $"Generate new donation link?",
|
||||
Description =
|
||||
"This previously linked donation instructions will be erased. If you paid anything to it, you will lose access.",
|
||||
Action = "Confirm and generate",
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
var httpClient = _httpClientFactory.CreateClient(EthereumLikeExtensions.EthereumInvoiceCreateHttpClient);
|
||||
string invoiceUrl;
|
||||
var response = await httpClient.PostAsync($"{Server.HexToUTF8String()}{invoiceEndpoint.HexToUTF8String()}",
|
||||
new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("choiceKey", $"license_{chainId}"),
|
||||
new KeyValuePair<string, string>("posData",
|
||||
JsonConvert.SerializeObject(new {Host = Request.Host, ChainId = chainId})),
|
||||
new KeyValuePair<string, string>("orderID", $"eth_{Request.Host}_{chainId}"),
|
||||
new KeyValuePair<string, string>("email", user.Email),
|
||||
new KeyValuePair<string, string>("redirectUrl",
|
||||
Url.Action("Callback", "EthereumConfig", new {chainId}, Request.Scheme)),
|
||||
}));
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Found)
|
||||
{
|
||||
HttpResponseHeaders headers = response.Headers;
|
||||
if (headers != null && headers.Location != null)
|
||||
{
|
||||
invoiceUrl = $"{Server.HexToUTF8String()}{headers.Location}";
|
||||
current.InvoiceId = headers.Location.ToString()
|
||||
.Replace("/i/", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
await UpdateChainConfig(chainId, current);
|
||||
return Redirect(invoiceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error, Message = $"Couldn't connect to donation server, try again later."
|
||||
});
|
||||
return RedirectToAction("UpdateChainConfig", new { chainId});
|
||||
}
|
||||
|
||||
private string invoiceEndpoint = "0x2f617070732f3262706f754e74576b4b3543636e426d374833456a3346505a756f412f706f73";
|
||||
private static string Server = "0x68747470733a2f2f787061797365727665722e636f6d";
|
||||
public static NetworkType InvoiceEnforced = NetworkType.Mainnet;
|
||||
|
||||
public static async Task<bool> CheckValid(IHttpClientFactory httpClientFactory, NetworkType networkType, string invoiceId)
|
||||
{
|
||||
if (networkType != InvoiceEnforced)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (string.IsNullOrEmpty(invoiceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient(EthereumLikeExtensions.EthereumInvoiceCheckHttpClient);
|
||||
var url = $"{Server.HexToUTF8String()}/i/{invoiceId}/status";
|
||||
var response = await httpClient.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
var status = JObject.Parse(raw)["status"].ToString();
|
||||
return (status.Equals("complete", StringComparison.InvariantCultureIgnoreCase) ||
|
||||
status.Equals("confirmed", StringComparison.InvariantCultureIgnoreCase));
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,281 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Filters;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum.Payments;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using NBitcoin;
|
||||
using Nethereum.HdWallet;
|
||||
using Nethereum.Hex.HexConvertors.Extensions;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
{
|
||||
[Route("stores/{storeId}/ethlike")]
|
||||
[OnlyIfSupportEth]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public class EthereumLikeStoreController : Controller
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
|
||||
public EthereumLikeStoreController(StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
private StoreData StoreData => HttpContext.GetStoreData();
|
||||
|
||||
[HttpGet()]
|
||||
public IActionResult GetStoreEthereumLikePaymentMethods()
|
||||
{
|
||||
var eth = StoreData.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<EthereumSupportedPaymentMethod>();
|
||||
|
||||
var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var ethNetworks = _btcPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>();
|
||||
|
||||
var vm = new ViewEthereumStoreOptionsViewModel() { };
|
||||
|
||||
foreach (var network in ethNetworks)
|
||||
{
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, EthereumPaymentType.Instance);
|
||||
var matchedPaymentMethod = eth.SingleOrDefault(method =>
|
||||
method.PaymentId == paymentMethodId);
|
||||
vm.Items.Add(new ViewEthereumStoreOptionItemViewModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
Enabled = matchedPaymentMethod != null && !excludeFilters.Match(paymentMethodId),
|
||||
IsToken = network is ERC20BTCPayNetwork,
|
||||
RootAddress = matchedPaymentMethod?.GetWalletDerivator()?.Invoke(0) ?? "not configured"
|
||||
});
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{cryptoCode}")]
|
||||
public IActionResult GetStoreEthereumLikePaymentMethod(string cryptoCode)
|
||||
{
|
||||
var eth = StoreData.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<EthereumSupportedPaymentMethod>();
|
||||
|
||||
var network = _btcPayNetworkProvider.GetNetwork<EthereumBTCPayNetwork>(cryptoCode);
|
||||
if (network is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, EthereumPaymentType.Instance);
|
||||
var matchedPaymentMethod = eth.SingleOrDefault(method =>
|
||||
method.PaymentId == paymentMethodId);
|
||||
|
||||
return View(new EditEthereumPaymentMethodViewModel()
|
||||
{
|
||||
Enabled = !excludeFilters.Match(paymentMethodId),
|
||||
XPub = matchedPaymentMethod?.XPub,
|
||||
Index = matchedPaymentMethod?.CurrentIndex ?? 0,
|
||||
Passphrase = matchedPaymentMethod?.Password,
|
||||
Seed = matchedPaymentMethod?.Seed,
|
||||
StoreSeed = !string.IsNullOrEmpty(matchedPaymentMethod?.Seed),
|
||||
OriginalIndex = matchedPaymentMethod?.CurrentIndex ?? 0,
|
||||
KeyPath = string.IsNullOrEmpty(matchedPaymentMethod?.KeyPath)
|
||||
? network.GetDefaultKeyPath()
|
||||
: matchedPaymentMethod?.KeyPath
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{cryptoCode}")]
|
||||
public async Task<IActionResult> GetStoreEthereumLikePaymentMethod(string cryptoCode,
|
||||
EditEthereumPaymentMethodViewModel viewModel)
|
||||
{
|
||||
var network = _btcPayNetworkProvider.GetNetwork<EthereumBTCPayNetwork>(cryptoCode);
|
||||
if (network is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var store = StoreData;
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, EthereumPaymentType.Instance);
|
||||
|
||||
var currentPaymentMethod = StoreData.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<EthereumSupportedPaymentMethod>().SingleOrDefault(method =>
|
||||
method.PaymentId == paymentMethodId);
|
||||
|
||||
if (currentPaymentMethod != null && currentPaymentMethod.CurrentIndex != viewModel.Index &&
|
||||
viewModel.OriginalIndex == viewModel.Index)
|
||||
{
|
||||
viewModel.Index = currentPaymentMethod.CurrentIndex;
|
||||
viewModel.OriginalIndex = currentPaymentMethod.CurrentIndex;
|
||||
}
|
||||
else if (currentPaymentMethod != null && currentPaymentMethod.CurrentIndex != viewModel.Index &&
|
||||
viewModel.OriginalIndex != currentPaymentMethod.CurrentIndex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.Index),
|
||||
$"You tried to update the index (to {viewModel.Index}) but new derivations in the background updated the index (to {currentPaymentMethod.CurrentIndex}) ");
|
||||
viewModel.Index = currentPaymentMethod.CurrentIndex;
|
||||
viewModel.OriginalIndex = currentPaymentMethod.CurrentIndex;
|
||||
}
|
||||
|
||||
Wallet wallet = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(viewModel.Seed))
|
||||
{
|
||||
wallet = new Wallet(viewModel.Seed, viewModel.Passphrase,
|
||||
string.IsNullOrEmpty(viewModel.KeyPath) ? network.GetDefaultKeyPath() : viewModel.KeyPath);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.Seed), $"seed was incorrect");
|
||||
}
|
||||
|
||||
if (wallet != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
wallet.GetAccount(0);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.KeyPath), $"keypath was incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
PublicWallet publicWallet = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(viewModel.XPub))
|
||||
{
|
||||
try
|
||||
{
|
||||
publicWallet = new PublicWallet(viewModel.XPub);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
publicWallet = new PublicWallet(new BitcoinExtPubKey(viewModel.XPub, Network.Main).ExtPubKey);
|
||||
}
|
||||
|
||||
if (wallet != null && !publicWallet.ExtPubKey.Equals(wallet.GetMasterPublicWallet().ExtPubKey))
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.XPub),
|
||||
$"The xpub does not match the seed/pass/key path provided");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.XPub), $"xpub was incorrect");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(viewModel.AddressCheck))
|
||||
{
|
||||
int index = -1;
|
||||
if (wallet != null)
|
||||
{
|
||||
index = wallet.GetAddresses(1000)
|
||||
.IndexOf(viewModel.AddressCheck);
|
||||
}
|
||||
else if (publicWallet != null)
|
||||
{
|
||||
index = publicWallet.GetAddresses(1000)
|
||||
.IndexOf(viewModel.AddressCheck);
|
||||
}
|
||||
|
||||
if (viewModel.AddressCheckLastUsed && index > -1)
|
||||
{
|
||||
viewModel.Index = index;
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.AddressCheck),
|
||||
"Could not confirm address belongs to configured wallet");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
currentPaymentMethod ??= new EthereumSupportedPaymentMethod();
|
||||
currentPaymentMethod.Password = viewModel.StoreSeed ? viewModel.Passphrase : "";
|
||||
currentPaymentMethod.Seed = viewModel.StoreSeed ? viewModel.Seed : "";
|
||||
currentPaymentMethod.XPub = string.IsNullOrEmpty(viewModel.XPub) && wallet != null
|
||||
? wallet.GetMasterPublicWallet().ExtPubKey.ToBytes().ToHex()
|
||||
: viewModel.XPub;
|
||||
currentPaymentMethod.CryptoCode = cryptoCode;
|
||||
currentPaymentMethod.KeyPath = string.IsNullOrEmpty(viewModel.KeyPath)
|
||||
? network.GetDefaultKeyPath()
|
||||
: viewModel.KeyPath;
|
||||
currentPaymentMethod.CurrentIndex = viewModel.Index;
|
||||
|
||||
blob.SetExcluded(paymentMethodId, !viewModel.Enabled);
|
||||
store.SetSupportedPaymentMethod(currentPaymentMethod);
|
||||
store.SetStoreBlob(blob);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = $"updated {cryptoCode}", Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
|
||||
return RedirectToAction("GetStoreEthereumLikePaymentMethods", new {storeId = store.Id});
|
||||
}
|
||||
}
|
||||
|
||||
public class EditEthereumPaymentMethodViewModel
|
||||
{
|
||||
public string XPub { get; set; }
|
||||
public string Seed { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
|
||||
public string KeyPath { get; set; }
|
||||
public long OriginalIndex { get; set; }
|
||||
|
||||
[Display(Name = "Current address index")]
|
||||
|
||||
public long Index { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[Display(Name = "Hot wallet")] public bool StoreSeed { get; set; }
|
||||
|
||||
[Display(Name ="Address Check")]
|
||||
public string AddressCheck { get; set; }
|
||||
|
||||
public bool AddressCheckLastUsed { get; set; }
|
||||
}
|
||||
|
||||
public class ViewEthereumStoreOptionsViewModel
|
||||
{
|
||||
public List<ViewEthereumStoreOptionItemViewModel> Items { get; set; } =
|
||||
new List<ViewEthereumStoreOptionItemViewModel>();
|
||||
}
|
||||
|
||||
public class ViewEthereumStoreOptionItemViewModel
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public bool IsToken { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string RootAddress { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,19 @@
|
|||
#if ALTCOINS
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
{
|
||||
public class EthereumPaymentViewModel
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string Confirmations { get; set; }
|
||||
public string DepositAddress { get; set; }
|
||||
public string Amount { get; set; }
|
||||
public DateTimeOffset ReceivedTime { get; set; }
|
||||
public long? BlockNumber { get; set; }
|
||||
public string BalanceLink { get; set; }
|
||||
public bool Replaced { get; set; }
|
||||
public long Index { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,100 @@
|
|||
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Services.Altcoins.Ethereum.UI.EditEthereumPaymentMethodViewModel
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
|
||||
ViewData["NavPartialName"] = "../Stores/_Nav";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.ActivePage, $"{this.Context.GetRouteValue("cryptoCode")} Settings");
|
||||
}
|
||||
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-warning">DO NOT USE THE WALLET TO ACCEPT PAYMENTS OUTSIDE OF THIS STORE.<br/>If you spend funds received on invoices which have not been marked complete yet, the invoice will be marked as unpaid.
|
||||
</div>
|
||||
<form method="post" asp-action="GetStoreEthereumLikePaymentMethod"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
|
||||
class="mt-4" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" asp-for="OriginalIndex"/>
|
||||
<div class="form-group">
|
||||
<label asp-for="Seed"></label>
|
||||
<input asp-for="Seed" type="text" class="form-control"/>
|
||||
<span asp-validation-for="Seed" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Passphrase"></label>
|
||||
<input type="text" asp-for="Passphrase" class="form-control" autocomplete="off"/>
|
||||
<span asp-validation-for="Passphrase" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="KeyPath"></label>
|
||||
<input asp-for="KeyPath" class="form-control"/>
|
||||
<span class="text-muted">Please see <a href="https://medium.com/myetherwallet/hd-wallets-and-derivation-paths-explained-865a643c7bf2" target="_blank">this article.</a></span>
|
||||
<span asp-validation-for="KeyPath" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreSeed"></label>
|
||||
<input asp-for="StoreSeed" type="checkbox" class="form-check"/>
|
||||
<span class="text-muted">Store the seed/password on server if provided. If not checked, will generate the xpub and erase the seed/pass from server</span>
|
||||
<span asp-validation-for="StoreSeed" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="XPub"></label>
|
||||
<input asp-for="XPub" class="form-control"/>
|
||||
<span class="text-muted">The public master key derived from a seed/pass/keypath. This allows you to generate addresses without private keys on the server.</span>
|
||||
<span asp-validation-for="XPub" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Index"></label>
|
||||
<input asp-for="Index" type="number" min="0" class="form-control"/>
|
||||
<span class="text-muted">The index to generate the next address from. If you are using a wallet that you have used before, be sure to set this to the last index +1</span>
|
||||
<span asp-validation-for="Index" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AddressCheck"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="AddressCheck" type="text" class="form-control"/>
|
||||
<div class="input-group-text">
|
||||
<input asp-for="AddressCheckLastUsed" type="checkbox" data-toggle="tooltip" class="form-check" title="Use this address to set the last used index for the wallet"/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-muted">Check wallet by providing an address it can generate within the first 1000 indexes</span>
|
||||
<span asp-validation-for="AddressCheck" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="Enabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
|
||||
|
||||
<a class="btn btn-secondary" asp-action="GetStoreEthereumLikePaymentMethods"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@this.Context.GetRouteValue("cryptoCode")"
|
||||
asp-controller="EthereumLikeStore">
|
||||
Back to list
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Services.Altcoins.Ethereum.UI.ViewEthereumStoreOptionsViewModel
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider;
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.ActivePage, "Ethereum Settings");
|
||||
|
||||
ViewData["NavPartialName"] = "../Stores/_Nav";
|
||||
}
|
||||
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Root address</th>
|
||||
<th class="text-center">Enabled</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.CryptoCode</td>
|
||||
<td>@item.RootAddress</td>
|
||||
<td class="text-center">
|
||||
@if (item.Enabled)
|
||||
{
|
||||
<span class="text-success fa fa-check"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-danger fa fa-times"></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a id="Modify@(item.CryptoCode)" asp-action="GetStoreEthereumLikePaymentMethod"
|
||||
asp-route-storeId="@this.Context.GetRouteValue("storeId")"
|
||||
asp-route-cryptoCode="@item.CryptoCode">
|
||||
Modify
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
var chains = BTCPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>().Select(network => network.ChainId).Distinct();
|
||||
foreach (var chain in chains)
|
||||
{
|
||||
|
||||
<a asp-action="UpdateChainConfig" asp-controller="EthereumConfig" asp-route-chainId="@chain">Configure Web3 for chain @chain</a>
|
||||
}
|
||||
}
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
<div class="nav flex-column nav-pills mb-4">
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||
<div class="nav flex-column nav-pills mb-4">
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="ListUsers">Users</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="LogsView">Logs</a>
|
||||
<a asp-controller="Server" class="nav-link @ViewData.IsActivePage(ServerNavPages.Files)" asp-action="Files">Files</a>
|
||||
</div>
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
<img v-bind:src="srvModel.cryptoImage"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separatorGem"></div>
|
||||
<div class="copySectionBox" :title="$t(hasPayjoin? 'BIP21 payment link' : 'BIP21 payment link with payjoin support') " >
|
||||
<div class="separatorGem" v-if="srvModel.invoiceBitcoinUrl"></div>
|
||||
<div class="copySectionBox" v-if="srvModel.invoiceBitcoinUrl" :title="$t(hasPayjoin? 'BIP21 payment link' : 'BIP21 payment link with payjoin support') " >
|
||||
<label>{{$t("Payment link")}}</label>
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.invoiceBitcoinUrl" readonly="readonly"/>
|
||||
|
|
27
BTCPayServer/Views/Shared/Ethereum/ETHSyncSummary.cshtml
Normal file
27
BTCPayServer/Views/Shared/Ethereum/ETHSyncSummary.cshtml
Normal file
|
@ -0,0 +1,27 @@
|
|||
@using BTCPayServer.Services.Altcoins.Ethereum.Services
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@inject EthereumService EthereumService;
|
||||
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@{
|
||||
var networks = BTCPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>().OrderBy(network => network.ChainId).Where(network => network.ShowSyncSummary);
|
||||
}
|
||||
|
||||
@foreach (var network in networks)
|
||||
{
|
||||
<h4>@network.CryptoCode (Chain ID: @network.ChainId) @(network is ERC20BTCPayNetwork is true ? "(ERC20)" : "")</h4>
|
||||
<ul>
|
||||
@if (!EthereumService.IsAvailable(network.CryptoCode, out var error))
|
||||
{
|
||||
<li>
|
||||
@(error??"Web3 has not yet been configured")
|
||||
|
||||
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<a asp-action="UpdateChainConfig" asp-controller="EthereumConfig" asp-route-chainId="@network.ChainId">Configure Web3</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
@using BTCPayServer.Services
|
||||
@model BTCPayServer.Models.InvoicingModels.PaymentModel
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider
|
||||
@{
|
||||
var chains = BTCPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>().ToDictionary(network => network.CryptoCode.ToLowerInvariant(), network => new
|
||||
{
|
||||
ChainId = network.ChainId,
|
||||
SmartContractAddress = (network as ERC20BTCPayNetwork)?.SmartContractAddress,
|
||||
Divisibility = network.Divisibility
|
||||
});
|
||||
}
|
||||
<script type="text/x-template" id="ethereum-method-checkout-template">
|
||||
<div>
|
||||
<div class="bp-view payment scan" id="scan" v-bind:class="{ 'active': currentTab == 'scan'}">
|
||||
|
||||
<div class="payment__scan">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" v-if="scanDisplayQr"/>
|
||||
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg" v-if="scanDisplayQr"></qrcode>
|
||||
<div class="payment__spinner qr_currency_icon" style="padding-right: 20px;">
|
||||
<partial name="Checkout-Spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet" v-if="hasWeb3">
|
||||
<button :disabled="web3Loading" class="payment__details__instruction__open-wallet__btn action-button" target="_top" v-on:click="payWithWeb3">
|
||||
<span>{{$t("Open in wallet")}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view payment manual-flow" id="copy" v-bind:class="{ 'active': currentTab == 'copy'}">
|
||||
<div class="manual__step-two__instructions">
|
||||
<span v-html="$t('CompletePay_Body', srvModel)"></span>
|
||||
</div>
|
||||
<div class="copyLabelPopup">
|
||||
<span>{{$t("Copied")}}</span>
|
||||
</div>
|
||||
<nav class="copyBox">
|
||||
<div class="copySectionBox bottomBorder">
|
||||
<label>{{$t("Amount")}}</label>
|
||||
<div class="copyAmountText copy-cursor _copySpan">
|
||||
<span>{{dueAmount}}</span> {{ srvModel.cryptoCode }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="separatorGem"></div>
|
||||
<div class="copySectionBox bottomBorder">
|
||||
<label>{{$t("Address")}}</label>
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly"/>
|
||||
<img v-bind:src="srvModel.cryptoImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
|
||||
{
|
||||
<div id="altcoins" class="bp-view payment manual-flow" v-bind:class="{ 'active': currentTab == 'altcoins'}">
|
||||
<nav >
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_BodyTop", srvModel)}}
|
||||
<br/><br/>
|
||||
{{$t("ConversionTab_BodyDesc", srvModel)}}
|
||||
</span>
|
||||
</div>
|
||||
<center>
|
||||
@if (Model.CoinSwitchEnabled && Model.ChangellyEnabled)
|
||||
{
|
||||
<template v-if="!selectedThirdPartyProcessor">
|
||||
<button v-on:click="selectedThirdPartyProcessor = 'coinswitch'" class="action-button">{{$t("Pay with CoinSwitch")}}</button>
|
||||
<button v-on:click="selectedThirdPartyProcessor = 'changelly'" class="action-button">{{$t("Pay with Changelly")}}</button>
|
||||
</template>
|
||||
}
|
||||
@if (Model.CoinSwitchEnabled)
|
||||
{
|
||||
<coinswitch inline-template
|
||||
v-if="!srvModel.changellyEnabled || selectedThirdPartyProcessor === 'coinswitch'"
|
||||
:mode="srvModel.coinSwitchMode"
|
||||
:merchant-id="srvModel.coinSwitchMerchantId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="coinswitchAmountDue"
|
||||
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">{{$t("Pay with CoinSwitch")}}</a>
|
||||
@if (Model.ChangellyEnabled)
|
||||
{
|
||||
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">{{$t("Pay with Changelly")}}</button>
|
||||
}
|
||||
<iframe
|
||||
v-if="showInlineIFrame"
|
||||
v-on:load="onLoadIframe"
|
||||
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
|
||||
:src="url">
|
||||
</iframe>
|
||||
</div>
|
||||
</coinswitch>
|
||||
}
|
||||
@if (Model.ChangellyEnabled)
|
||||
{
|
||||
<changelly inline-template
|
||||
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
|
||||
:merchant-id="srvModel.changellyMerchantId"
|
||||
:store-id="srvModel.storeId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.changellyAmountDue"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div class="changelly-component">
|
||||
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
|
||||
<select
|
||||
v-model="selectedFromCurrency"
|
||||
:disabled="isLoading"
|
||||
v-on:change="onCurrencyChange($event)"
|
||||
ref="changellyCurrenciesDropdown">
|
||||
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
|
||||
<option v-for="currency of currencies"
|
||||
:data-prefix="'<img src=\''+currency.image+'\'/>'"
|
||||
:value="currency.name">{{currency.fullName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">{{$t("Pay with Changelly")}}</a>
|
||||
@if (Model.CoinSwitchEnabled)
|
||||
{
|
||||
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">{{$t("Pay with CoinSwitch")}}</button>
|
||||
}
|
||||
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
|
||||
{{$t("ConversionTab_CalculateAmount_Error")}}
|
||||
</button>
|
||||
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
|
||||
{{$t("ConversionTab_LoadCurrencies_Error")}}
|
||||
</button>
|
||||
<div v-show="isLoading" class="general__spinner">
|
||||
<partial name="Checkout-Spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
</changelly>
|
||||
}
|
||||
</center>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="ethereum-method-checkout-header-template">
|
||||
<div class="payment-tabs">
|
||||
<div class="payment-tabs__tab " id="scan-tab" v-on:click="switchTab('scan')" v-bind:class="{ 'active': currentTab == 'scan'}" >
|
||||
<span>{{$t("Scan")}}</span>
|
||||
</div>
|
||||
<div class="payment-tabs__tab" id="copy-tab" v-on:click="switchTab('copy')" v-bind:class="{ 'active': currentTab == 'copy'}" >
|
||||
<span>{{$t("Copy")}}</span>
|
||||
</div>
|
||||
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
|
||||
{
|
||||
<div class="payment-tabs__tab" id="altcoins-tab" v-on:click="switchTab('altcoins')" v-bind:class="{ 'active': currentTab == 'altcoins'}" >
|
||||
<span>{{$t("Conversion")}}</span>
|
||||
</div>
|
||||
<div id="tabsSlider" class="payment-tabs__slider three-tabs" v-bind:class="['slide-'+currentTab]"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="tabsSlider" class="payment-tabs__slider" v-bind:class="['slide-'+currentTab]"></div>
|
||||
}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
Vue.component('EthereumLikeMethodCheckout',
|
||||
{
|
||||
props: ["srvModel"],
|
||||
template: "#ethereum-method-checkout-template",
|
||||
components: {
|
||||
qrcode: VueQrcode,
|
||||
changelly: ChangellyComponent,
|
||||
coinswitch: CoinSwitchComponent
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
cryptoChains: @Safe.Json(chains),
|
||||
selectedThirdPartyProcessor: "",
|
||||
currentTab: "scan",
|
||||
hasWeb3: false,
|
||||
web3Loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dueAmount: function(){
|
||||
var r = this.srvModel.btcDue;
|
||||
while(r.charAt(r.length-1)=='0') {
|
||||
r = r.substring(0,r.length-1);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
coinswitchAmountDue: function() {
|
||||
return this.srvModel.coinSwitchAmountMarkupPercentage
|
||||
? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
|
||||
: this.srvModel.btcDue;
|
||||
},
|
||||
scanDisplayQr: function() {
|
||||
return this.srvModel.invoiceBitcoinUrlQR;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
payWithWeb3: function() {
|
||||
var self = this;
|
||||
self.web3Loading = true;
|
||||
var chainData = self.cryptoChains[self.srvModel.cryptoCode.toLowerCase()];
|
||||
var ptio = self.dueAmount.indexOf('.');
|
||||
var amt = self.dueAmount.substr(0,ptio) + web3.padRight(self.dueAmount.substr(ptio+1), chainData.divisibility);
|
||||
if(chainData.smartContractAddress){
|
||||
var abi = [
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{ name: "_to", type: "address" },
|
||||
{ name: "_value", type: "uint256" },
|
||||
],
|
||||
name: "transfer",
|
||||
outputs: [{ name: "", type: "bool" }],
|
||||
type: "function",
|
||||
},
|
||||
];
|
||||
var contract = web3.eth.contract(abi).at(chainData.smartContractAddress);
|
||||
contract.transfer(self.srvModel.btcAddress, amt, (error, txHash) => {
|
||||
if(error){
|
||||
console.error(error);
|
||||
}
|
||||
self.web3Loading = false;
|
||||
});
|
||||
}else {
|
||||
web3.eth.sendTransaction({
|
||||
from : 0,
|
||||
to : self.srvModel.btcAddress,
|
||||
value: amt,
|
||||
},
|
||||
(error, txID) => {
|
||||
|
||||
if(error){
|
||||
console.error(error);
|
||||
}
|
||||
self.web3Loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
var self = this;
|
||||
eventBus.$on("tab-switched",
|
||||
function(tab) {
|
||||
self.currentTab = tab;
|
||||
});
|
||||
if (window.ethereum) {
|
||||
window.web3 = new Web3(ethereum);
|
||||
}
|
||||
|
||||
var chainData = self.cryptoChains[self.srvModel.cryptoCode.toLowerCase()];
|
||||
if (typeof web3 !== "undefined"){
|
||||
if(ethereum && ethereum.chainId){
|
||||
self.hasWeb3 = chainData.chainId == parseInt(ethereum.chainId, 16);
|
||||
}else if (web3.eth.getChainId){
|
||||
web3.eth.getChainId(function(chainId){
|
||||
self.hasWeb3 = chainData.chainId == chainId;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('EthereumLikeMethodCheckoutHeader', {
|
||||
props: ["srvModel"],
|
||||
template: "#ethereum-method-checkout-header-template",
|
||||
data: function() {
|
||||
return {
|
||||
currentTab: "scan"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
switchTab: function(tab) {
|
||||
this.currentTab = tab;
|
||||
eventBus.$emit("tab-switched", tab);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
// Clipboard Copy
|
||||
var copySpan = new Clipboard('._copySpan', {
|
||||
target: function(trigger) {
|
||||
return copyElement(trigger, 0, 65).firstChild;
|
||||
}
|
||||
});
|
||||
var copyInput = new Clipboard('._copyInput', {
|
||||
target: function(trigger) {
|
||||
return copyElement(trigger, 4, 65).firstChild;
|
||||
}
|
||||
});
|
||||
|
||||
function copyElement(trigger, popupLeftModifier, popupTopModifier) {
|
||||
var elm = $(trigger);
|
||||
var position = elm.offset();
|
||||
position.top -= popupLeftModifier + $(window).scrollTop();
|
||||
position.left += (elm.width() / 2) - popupTopModifier;
|
||||
$(".copyLabelPopup").css(position).addClass("copied");
|
||||
elm.removeClass("copy-cursor").addClass("clipboardCopied");
|
||||
setTimeout(clearSelection, 100);
|
||||
setTimeout(function() {
|
||||
elm.removeClass("clipboardCopied").addClass("copy-cursor");
|
||||
$(".copyLabelPopup").removeClass("copied");
|
||||
},
|
||||
1000);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
} else if (document.selection) {
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
||||
// Disable enter key
|
||||
$(document).keypress(
|
||||
function(event) {
|
||||
if (event.which === '13') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,11 @@
|
|||
@using BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider;
|
||||
@{
|
||||
var controller = ViewContext.RouteData.Values["Controller"].ToString();
|
||||
var isEthereum = controller.Equals(nameof(EthereumLikeStoreController), StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
@if (SignInManager.IsSignedIn(User) && BTCPayNetworkProvider.GetAll().OfType<EthereumBTCPayNetwork>().Any())
|
||||
{
|
||||
<a class="nav-link @(isEthereum ? "active" : string.Empty)" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-action="GetStoreEthereumLikePaymentMethods" asp-controller="EthereumLikeStore">Ethereum</a>
|
||||
}
|
65
BTCPayServer/Views/Shared/Ethereum/UpdateChainConfig.cshtml
Normal file
65
BTCPayServer/Views/Shared/Ethereum/UpdateChainConfig.cshtml
Normal file
|
@ -0,0 +1,65 @@
|
|||
@using BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
@using BTCPayServer.Views.Server
|
||||
@using System.Net.Http
|
||||
@model BTCPayServer.Services.Altcoins.Ethereum.Configuration.EthereumLikeConfiguration
|
||||
@inject BTCPayNetworkProvider BTCPayNetworkProvider;
|
||||
@inject IHttpClientFactory HttpClientFactory;
|
||||
@{
|
||||
Layout = "../_NavLayout.cshtml";
|
||||
ViewData["NavPartialName"] = "../Server/_Nav";
|
||||
ViewBag.MainTitle = "Server settings";
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Policies, $"ETH Chain {Model.ChainId} Configuration");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
@if (!this.ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<input type="hidden" asp-for="ChainId"/>
|
||||
<input type="hidden" asp-for="InvoiceId"/>
|
||||
<div class="form-check">
|
||||
<label asp-for="Web3ProviderUrl" class="form-control-label"></label>
|
||||
<input asp-for="Web3ProviderUrl" type="text" class="form-control"/>
|
||||
<span asp-validation-for="Web3ProviderUrl" class="text-danger"></span>
|
||||
<div>
|
||||
Possible free options are
|
||||
<ul>
|
||||
<li><a href="https://medium.com/linkpool/release-of-public-ethereum-rpcs-f5dd57455d2e" target="_blank">linkpool.io</a> - Free, just set the url to <code>https://main-rpc.linkpool.io</code></li>
|
||||
<li><a href="https://chainstack.com/" target="_blank">chainstack.com</a> - Free plan, choose shared public node</li>
|
||||
<li><a href="https://infura.io/" target="_blank">infura.io</a> - Free tier but limited calls per day</li>
|
||||
<li>Your own geth/openethereum node</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label asp-for="Web3ProviderUsername" class="form-control-label"></label>
|
||||
<input asp-for="Web3ProviderUsername" type="text" class="form-control"/>
|
||||
<span asp-validation-for="Web3ProviderUsername" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label asp-for="Web3ProviderPassword" class="form-control-label"></label>
|
||||
<input asp-for="Web3ProviderPassword" type="text" class="form-control"/>
|
||||
<span asp-validation-for="Web3ProviderPassword" class="text-danger"></span>
|
||||
</div>
|
||||
@{
|
||||
var valid = await EthereumConfigController.CheckValid(HttpClientFactory, BTCPayNetworkProvider.NetworkType, Model.InvoiceId);
|
||||
if (!valid)
|
||||
{
|
||||
<div class="alert alert-warning mt-2">
|
||||
<span>Support for this feature requires a one-time donation.</span>
|
||||
@if (!string.IsNullOrEmpty(Model.InvoiceId))
|
||||
{
|
||||
<span title="@Model.InvoiceId">The payment instructions associated has not been paid or confirmed yet.</span>
|
||||
}
|
||||
<a asp-action="CreateInvoice" asp-controller="EthereumConfig" asp-route-chainId="@Model.ChainId" class="alert-link">Please click here to generate payment instructions.</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save" id="saveButton">Save</button>
|
||||
</form>
|
|
@ -0,0 +1,64 @@
|
|||
@using System.Globalization
|
||||
@using BTCPayServer.Services.Altcoins.Ethereum.Payments
|
||||
@using BTCPayServer.Services.Altcoins.Ethereum.UI
|
||||
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||
|
||||
@{
|
||||
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == EthereumPaymentType.Instance).Select(payment =>
|
||||
{
|
||||
var m = new EthereumPaymentViewModel();
|
||||
var onChainPaymentData = payment.GetCryptoPaymentData() as EthereumLikePaymentData;
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.GetDestination();
|
||||
|
||||
m.Amount = onChainPaymentData.GetValue().ToString(CultureInfo.InvariantCulture);
|
||||
m.Confirmations = onChainPaymentData.BlockNumber.HasValue ? $"{onChainPaymentData.ConfirmationCount} (block {onChainPaymentData.BlockNumber})" : "pending";
|
||||
m.Amount = onChainPaymentData.GetValue().ToString(CultureInfo.InvariantCulture);
|
||||
m.BlockNumber = onChainPaymentData.BlockNumber;
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.BalanceLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.DepositAddress);
|
||||
m.Replaced = !payment.Accounted;
|
||||
m.Index = onChainPaymentData.AccountIndex;
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
@if (onchainPayments.Any())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12 invoice-payments">
|
||||
<h3>Ethereum/ERC-20 payments</h3>
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Amount</th>
|
||||
<th>Address</th>
|
||||
<th>Index</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in onchainPayments)
|
||||
{
|
||||
|
||||
<tr style="@(payment.Replaced ? "text-decoration: line-through" : "")">
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.Amount</td>
|
||||
<td>
|
||||
<div class="wraptextAuto">
|
||||
<a href="@payment.BalanceLink" target="_blank">
|
||||
|
||||
@payment.DepositAddress
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>@payment.Index</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
BIN
BTCPayServer/wwwroot/imlegacy/eth.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/eth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Loading…
Add table
Reference in a new issue