Squashed: Add prototype

This commit is contained in:
Ivan Yaremenchuk 2022-03-31 22:16:43 +02:00
parent 3bc467e1d3
commit 69e5d18d3a
63 changed files with 5428 additions and 274 deletions

View File

@ -0,0 +1,12 @@
using NftFaucet.ApiClients.NftStorage.Models;
using RestEase;
namespace NftFaucet.ApiClients.NftStorage;
[BaseAddress("https://api.nft.storage")]
[Header("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweGQxQTdlMDk3QjdEOTNGZURkNTU1RTE1M2FGMzg4OTkwQzBGQ0EwY2MiLCJpc3MiOiJuZnQtc3RvcmFnZSIsImlhdCI6MTY0ODgxNTYxODkxOSwibmFtZSI6IlRlc3QifQ.CqCC_jo8TIsA1JMGC_NsthRAQKSCJbKjSp5irZwr54g")]
public interface INftStorageClient
{
[Post("upload")]
Task<UploadResponse> UploadFile([Body] MultipartContent content);
}

View File

@ -0,0 +1,21 @@
namespace NftFaucet.ApiClients.NftStorage.Models;
public class UploadResponse
{
public bool Ok { get; set; }
public NftStorageResponseValue Value { get; set; }
public class NftStorageResponseValue
{
public string Cid { get; set; }
public string Type { get; set; }
public NftStorageFile[] Files { get; set; }
public long Size { get; set; }
}
public class NftStorageFile
{
public string Name { get; set; }
public string Type { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace NftFaucet.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class FunctionHashAttribute : Attribute
{
public string Hash { get; set; }
public FunctionHashAttribute(string hash)
{
if (string.IsNullOrEmpty(hash))
throw new ArgumentNullException(nameof(hash));
Hash = hash;
}
}

View File

@ -0,0 +1,43 @@
using AntDesign;
using Microsoft.AspNetCore.Components;
using NftFaucet.Models;
using NftFaucet.Services;
namespace NftFaucet.Components;
public abstract class BasicComponent : ComponentBase
{
[Inject]
protected NavigationManager UriHelper { get; set; }
[Inject]
protected ScopedAppState AppState { get; set; }
[Inject]
protected RefreshMediator RefreshMediator { get; set; }
[Inject]
public MessageService MessageService { get; set; }
[Inject]
public IIpfsService IpfsService { get; set; }
protected MetamaskInfo Metamask => AppState?.Metamask;
protected override void OnInitialized()
{
RefreshMediator.StateChanged += async () => await InvokeAsync(StateHasChangedSafe);
}
protected void StateHasChangedSafe()
{
try
{
InvokeAsync(StateHasChanged);
}
catch (Exception)
{
// ignored
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Components;
using NftFaucet.Models;
namespace NftFaucet.Components;
public abstract class LayoutBasicComponent : LayoutComponentBase
{
[Inject]
protected NavigationManager UriHelper { get; set; }
[Inject]
protected ScopedAppState AppState { get; set; }
[Inject]
protected RefreshMediator RefreshMediator { get; set; }
protected override void OnInitialized()
{
RefreshMediator.StateChanged += async () => await InvokeAsync(StateHasChangedSafe);
}
protected void StateHasChangedSafe()
{
try
{
InvokeAsync(StateHasChanged);
}
catch (Exception)
{
// ignored
}
}
}

View File

@ -0,0 +1,37 @@
@inherits ComponentBase
@code
{
[Parameter] public string Icon { get; set; }
[Parameter] public string Text { get; set; }
[Parameter] public string Color { get; set; } = "black";
[Parameter] public int? Level { get; set; }
[Parameter] public string FontSize { get; set; } = "1em";
private string IconStyle => $"display: inline; font-size: {FontSize};";
private string TitleStyle => $"color: {Color};" + (Level.HasValue ? "margin-bottom: 0;" : $"font-size: {FontSize};");
}
<Space Align="center">
@if (!string.IsNullOrEmpty(Icon))
{
<SpaceItem>
<div class="space-item">
<Icon Type="@Icon" Style="@IconStyle"></Icon>
</div>
</SpaceItem>
}
@if (!string.IsNullOrEmpty(Text))
{
if (Level == null)
{
<SpaceItem>
<Text Style="@TitleStyle">@Text</Text>
</SpaceItem>
}
else
{
<Title Level="@Level.Value" Style="@TitleStyle">@Text</Title>
}
}
</Space>

View File

@ -0,0 +1,4 @@
.space-item {
align-items: center !important;
display: flex !important;
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Constants;
public static class UploadConstants
{
public const int MaxFileSizeInMegabytes = 20;
public const long MaxFileSizeInBytes = MaxFileSizeInMegabytes * 1024 * 1024;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts@4.5.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@4.5.0/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts@4.5.0/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts@4.5.0/access/Ownable.sol";
import "@openzeppelin/contracts@4.5.0/utils/Counters.sol";
contract NftFaucet is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("NftFaucet", "NFAU") {}
function safeMint(address to, string memory uri) public {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NftFaucet.Extensions;
public static class StringExtensions
{
public static bool IsValidJson(this string str)
{
if (string.IsNullOrWhiteSpace(str))
return false;
str = str.Trim();
if ((!str.StartsWith("{") || !str.EndsWith("}")) && (!str.StartsWith("[") || !str.EndsWith("]")))
return false;
try
{
var _ = JToken.Parse(str);
return true;
}
catch (JsonReaderException)
{
return false;
}
}
}

View File

@ -0,0 +1,14 @@
using System.Reflection;
namespace NftFaucet.Extensions;
public static class TypeExtensions
{
public static TAttribute GetAttribute<TAttribute>(this MemberInfo memberInfo)
where TAttribute : Attribute
{
var customAttributes = memberInfo?.GetCustomAttributes(false);
var attribute = customAttributes?.OfType<TAttribute>().SingleOrDefault();
return attribute;
}
}

View File

@ -0,0 +1,46 @@
using CSharpFunctionalExtensions;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Util;
namespace NftFaucet.Models;
public class Address : ValueObject<Address>
{
private const string LongFormatPrefix = "0x000000000000000000000000";
private Address(string value)
{
Value = value;
}
public string Value { get; }
public static implicit operator string(Address address) => address.Value;
public static explicit operator Address(string address) => Create(address).Value;
public static Result<Address> Create(string address)
{
const int longFormatLength = 66;
if (address.IsAnEmptyAddress())
return Result.Failure<Address>("Address is empty");
address = address.EnsureHexPrefix();
if (address.Length == longFormatLength && address.StartsWith(LongFormatPrefix))
address = address.Substring(LongFormatPrefix.Length).EnsureHexPrefix();
if (!address.IsValidEthereumAddressHexFormat() || !address.IsValidEthereumAddressLength())
return Result.Failure<Address>("Invalid address value");
return new Address(address);
}
public override string ToString() => Value;
public string ToShortFormatString() => ToString();
public string ToLongFormatString() => LongFormatPrefix + Value.RemoveHexPrefix();
protected override bool EqualsCore(Address other)
=> string.Equals(Value, other.Value, StringComparison.InvariantCultureIgnoreCase);
protected override int GetHashCodeCore() => Value.GetHashCode(StringComparison.InvariantCultureIgnoreCase);
}

View File

@ -0,0 +1,15 @@
namespace NftFaucet.Models.Enums;
public class EnumWrapper<T> where T : Enum
{
public T Value { get; set; }
public string ValueString { get; set; }
public string Description { get; set; }
public EnumWrapper(T value, string description)
{
Value = value;
ValueString = value.ToString();
Description = description;
}
}

View File

@ -0,0 +1,12 @@
namespace NftFaucet.Models.Enums;
public enum EthereumNetwork : long
{
EthereumMainnet = 1,
Ropsten = 3,
Rinkeby = 4,
Goerli = 5,
Kovan = 42,
PolygonMainnet = 137,
PolygonMumbai = 80001,
}

View File

@ -0,0 +1,9 @@
namespace NftFaucet.Models.Enums;
public enum IpfsGatewayType : byte
{
None = 0,
IpfsOfficial = 1,
Infura = 2,
NftStorage = 3,
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Models.Enums;
public enum TokenType : byte
{
ERC721 = 0,
ERC1155 = 1,
}

View File

@ -0,0 +1,18 @@
using System.Numerics;
using Nethereum.ABI.FunctionEncoding.Attributes;
using NftFaucet.Attributes;
namespace NftFaucet.Models.Function;
[Function("mint"), FunctionHash("0xd3fc9864")]
public class Erc1155MintFunction : Function
{
[Parameter("address", "to", 1)]
public string To { get; set; }
[Parameter("uint256", "amount", 2)]
public BigInteger Amount { get; set; }
[Parameter("string", "tokenUri", 3)]
public string Uri { get; set; }
}

View File

@ -0,0 +1,14 @@
using Nethereum.ABI.FunctionEncoding.Attributes;
using NftFaucet.Attributes;
namespace NftFaucet.Models.Function;
[Function("safeMint"), FunctionHash("0xd204c45e")]
public class Erc721MintFunction : Function
{
[Parameter("address", "to", 1)]
public string To { get; set; }
[Parameter("string", "uri", 2)]
public string Uri { get; set; }
}

View File

@ -0,0 +1,20 @@
using Nethereum.ABI.FunctionEncoding;
using Nethereum.Hex.HexConvertors.Extensions;
using NftFaucet.Attributes;
using NftFaucet.Extensions;
namespace NftFaucet.Models.Function;
public abstract class Function
{
public string Encode()
{
var hash = GetHash().EnsureHexPrefix();
var encoder = new FunctionCallEncoder();
var encodedParameters = encoder.EncodeParametersFromTypeAttributes(GetType(), this);
var encodedCall = encoder.EncodeRequest(hash, encodedParameters.ToHex());
return encodedCall;
}
public string GetHash() => GetType().GetAttribute<FunctionHashAttribute>().Hash;
}

View File

@ -0,0 +1,96 @@
using MetaMask.Blazor;
using MetaMask.Blazor.Enums;
using NftFaucet.Models.Enums;
using NftFaucet.Utils;
using Serilog;
namespace NftFaucet.Models;
public class MetamaskInfo
{
private readonly RefreshMediator _refreshMediator;
public MetamaskInfo(MetaMaskService service, RefreshMediator refreshMediator)
{
Service = service;
_refreshMediator = refreshMediator;
}
public MetaMaskService Service { get; }
public bool? HasMetaMask { get; private set; }
public bool? IsMetaMaskConnected { get; private set; }
public string Address { get; private set; }
public long ChainId { get; private set; }
public EthereumNetwork? Network { get; private set; }
public async Task<bool> IsConnected()
{
HasMetaMask ??= await Service.HasMetaMask();
IsMetaMaskConnected ??= await Service.IsSiteConnected();
return HasMetaMask.Value && IsMetaMaskConnected.Value;
}
public async Task<bool> IsReady()
{
if (!await IsConnected())
return false;
if (string.IsNullOrEmpty(Address) || ChainId == 0)
{
await RefreshAddress();
SubscribeToEvents();
}
return HasMetaMask!.Value && IsMetaMaskConnected!.Value && !string.IsNullOrEmpty(Address) && ChainId != 0;
}
public async Task<bool> Connect()
{
var result = await ResultWrapper.Wrap(() => Service.ConnectMetaMask());
if (result.IsFailure)
{
Log.Error(result.Error);
return false;
}
HasMetaMask = true;
IsMetaMaskConnected = true;
await RefreshAddress();
SubscribeToEvents();
return true;
}
public async Task RefreshAddress()
{
Address = await Service.GetSelectedAddress();
ChainId = (await Service.GetSelectedChain()).chainId;
Network = Enum.IsDefined(typeof(EthereumNetwork), ChainId) ? (EthereumNetwork) ChainId : null;
_refreshMediator.NotifyStateHasChangedSafe();
}
private void SubscribeToEvents()
{
MetaMaskService.AccountChangedEvent += OnAccountChangedEvent;
MetaMaskService.ChainChangedEvent += OnChainChangedEvent;
Service.ListenToEvents();
}
private Task OnAccountChangedEvent(string newAddress)
{
Address = newAddress;
_refreshMediator.NotifyStateHasChangedSafe();
return Task.CompletedTask;
}
private Task OnChainChangedEvent((long ChainId, Chain Chain) arg)
{
ChainId = arg.ChainId;
Network = Enum.IsDefined(typeof(EthereumNetwork), ChainId) ? (EthereumNetwork) ChainId : null;
_refreshMediator.NotifyStateHasChangedSafe();
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Components;
namespace NftFaucet.Models;
public class NavigationWrapper
{
private readonly NavigationManager _uriHelper;
private string _currentUri;
private int _currentStep;
private Func<Task<bool>> _beforeGoBack;
private Func<Task<bool>> _beforeGoForward;
public NavigationWrapper(NavigationManager uriHelper)
{
_uriHelper = uriHelper;
}
public int CurrentStep
{
get
{
var uri = _uriHelper.ToBaseRelativePath(_uriHelper.Uri);
if (uri == _currentUri)
return _currentStep;
_currentUri = uri;
if (!_currentUri.StartsWith("step") || uri.Length < 5)
{
_currentStep = 1;
}
else
{
_currentStep = int.Parse(uri.Substring(4).First().ToString());
}
return _currentStep;
}
}
public void SetBackHandler(Func<Task<bool>> handler)
{
_beforeGoBack = handler;
}
public void SetForwardHandler(Func<Task<bool>> handler)
{
_beforeGoForward = handler;
}
public async Task GoBack()
{
if (_beforeGoBack != null)
{
var shouldGoBack = await _beforeGoBack();
if (!shouldGoBack)
return;
}
var previousStep = CurrentStep - 1;
_beforeGoBack = null;
_beforeGoForward = null;
_uriHelper.NavigateTo("/step" + previousStep);
}
public async Task GoForward()
{
if (_beforeGoForward != null)
{
var shouldGoForward = await _beforeGoForward();
if (!shouldGoForward)
return;
}
var nextStep = CurrentStep + 1;
_beforeGoBack = null;
_beforeGoForward = null;
_uriHelper.NavigateTo("/step" + nextStep);
}
}

View File

@ -0,0 +1,19 @@
namespace NftFaucet.Models;
public class RefreshMediator
{
public delegate void StateChangedDelegate();
public event StateChangedDelegate StateChanged;
public void NotifyStateHasChangedSafe()
{
try
{
StateChanged?.Invoke();
}
catch (Exception)
{
// ignored
}
}
}

View File

@ -0,0 +1,19 @@
namespace NftFaucet.Models;
public class ScopedAppState
{
public ScopedAppState(MetamaskInfo metamask, NavigationWrapper navigationWrapper)
{
Metamask = metamask;
Navigation = navigationWrapper;
}
public MetamaskInfo Metamask { get; }
public NavigationWrapper Navigation { get; }
public StateStorage Storage { get; private set; } = new();
public void Reset()
{
Storage = new StateStorage();
}
}

View File

@ -0,0 +1,19 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Models;
public class StateStorage
{
public string TokenName { get; set; }
public string TokenDescription { get; set; }
public IpfsGatewayType IpfsGatewayType { get; set; } = IpfsGatewayType.NftStorage;
public TokenType TokenType { get; set; } = TokenType.ERC721;
public double TokenAmount { get; set; } = 1;
public Uri LocalImageUrl { get; set; }
public bool CanPreviewTokenFile { get; set; }
public bool UploadIsInProgress { get; set; }
public Uri IpfsImageUrl { get; set; }
public string TokenMetadata { get; set; }
public string TokenUrl { get; set; }
public string DestinationAddress { get; set; }
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace NftFaucet.Models.Token;
public class TokenMetadata
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("image")]
public string Image { get; set; }
[JsonProperty("external_url")]
public string ExternalUrl { get; set; }
}

View File

@ -2,16 +2,29 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AntDesign" Version="0.10.6-alpha.2" />
<PackageReference Include="BlazorMonaco" Version="2.1.0" />
<PackageReference Include="CSharpFunctionalExtensions" Version="2.29.0" />
<PackageReference Include="MetaMask.Blazor" Version="1.6.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
<PackageReference Include="RestEase" Version="1.5.5" />
<PackageReference Include="Serilog.Sinks.BrowserConsole" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Options;
public class EthereumNetworkOptions
{
public EthereumNetwork Id { get; set; }
public string Erc721ContractAddress { get; set; }
public string Erc1155ContractAddress { get; set; }
}

View File

@ -0,0 +1,9 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Options;
public class IpfsGatewayOptions
{
public IpfsGatewayType Id { get; set; }
public string BaseUrl { get; set; }
}

View File

@ -0,0 +1,15 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Options;
public class Settings
{
public EthereumNetworkOptions[] EthereumNetworks { get; set; }
public IpfsGatewayOptions[] IpfsGateways { get; set; }
public EthereumNetworkOptions GetEthereumNetworkOptions(EthereumNetwork network)
=> EthereumNetworks.FirstOrDefault(x => x.Id == network);
public IpfsGatewayOptions GetIpfsGatewayOptions(IpfsGatewayType gateway)
=> IpfsGateways.FirstOrDefault(x => x.Id == gateway);
}

View File

@ -0,0 +1,7 @@
@page "/connect-metamask"
@layout EmptyLayout
@inherits ConnectMetamaskComponent
<h1>You should connect MetaMask first!</h1>
<Button Type="@ButtonType.Primary" OnClick="Connect">Connect</Button>

View File

@ -0,0 +1,24 @@
using NftFaucet.Components;
namespace NftFaucet.Pages;
public class ConnectMetamaskComponent : BasicComponent
{
protected override async Task OnInitializedAsync()
{
if (await Metamask.IsConnected())
{
await Metamask.RefreshAddress();
UriHelper.NavigateTo("/");
}
}
protected async Task Connect()
{
var isConnected = await Metamask.Connect();
if (isConnected)
{
UriHelper.NavigateTo("/");
}
}
}

View File

@ -1,19 +0,0 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@ -1,60 +0,0 @@
@page "/fetchdata"
@inject HttpClient Http
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
}
}

View File

@ -1,9 +1,5 @@
@page "/"
@layout EmptyLayout
@inherits IndexComponent
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?"/>
<Text>Redirecting...</Text>

View File

@ -0,0 +1,11 @@
using NftFaucet.Components;
namespace NftFaucet.Pages;
public class IndexComponent : BasicComponent
{
protected override async Task OnInitializedAsync()
{
UriHelper.NavigateTo(await Metamask.IsReady() ? "/step1" : "/connect-metamask");
}
}

View File

@ -0,0 +1,111 @@
@page "/step1"
@using NftFaucet.Models.Enums
@using Microsoft.AspNetCore.Components
@inherits Step1Component
<Space Align="center" Direction="DirectionVHType.Vertical" Class="drk-vertical-space-center">
<SpaceItem Class="drk-full-width">
<Space Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem>
<Upload Name="file" Class="@ImageClass" ListType="picture-card"
ShowUploadList="false" BeforeAllUploadAsync="BeforeUpload">
@if (AppState?.Storage?.UploadIsInProgress ?? false)
{
<div>
<Icon Spin="true" Type="loading"></Icon>
<div className="ant-upload-text">Uploading...</div>
</div>
}
else if (AppState?.Storage?.LocalImageUrl != null && AppState.Storage.CanPreviewTokenFile)
{
<img src="@AppState?.Storage?.LocalImageUrl" alt="avatar" style="width: 100%"/>
}
else if (AppState?.Storage?.LocalImageUrl != null)
{
<div>
<Icon Type="eye-invisible" Theme="outline"/>
<div className="ant-upload-text">Unable to preview</div>
</div>
}
else
{
<div>
<Icon Type="plus"></Icon>
<div className="ant-upload-text">Upload</div>
</div>
}
</Upload>
</SpaceItem>
<SpaceItem Class="drk-grow">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-vertical-space">
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Name:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@NameClass">
<Input Size="medium" @bind-Value="@AppState.Storage.TokenName" OnInput="@OnNameInputChange" />
</div>
</SpaceItem>
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Description:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@DescriptionClass">
<TextArea ShowCount MaxLength="255" MinRows="3" MaxRows="5" OnInput="@OnDescriptionInputChange" @bind-Value="@AppState.Storage.TokenDescription" />
</div>
</SpaceItem>
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">IPFS Gateway:</Title>
</SpaceItem>
<SpaceItem>
<Select DataSource="@IpfsGateways"
DefaultValue="@(nameof(IpfsGatewayType.NftStorage))"
ValueName="@nameof(EnumWrapper<IpfsGatewayType>.ValueString)"
LabelName="@nameof(EnumWrapper<IpfsGatewayType>.Description)"
OnSelectedItemChanged="OnIpfsGatewayChange">
</Select>
</SpaceItem>
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token type:</Title>
</SpaceItem>
<SpaceItem>
<Select DataSource="@TokenTypes"
DefaultValue="@(nameof(TokenType.ERC721))"
ValueName="@nameof(EnumWrapper<TokenType>.ValueString)"
LabelName="@nameof(EnumWrapper<TokenType>.Description)"
OnSelectedItemChanged="OnTokenTypeChange">
</Select>
</SpaceItem>
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Amount:</Title>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.InputNumber @bind-Value="@AppState.Storage.TokenAmount" Disabled="@(AppState.Storage.TokenType == TokenType.ERC721)" Min="1" Max="1000" DefaultValue="1"></AntDesign.InputNumber>
</div>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -0,0 +1,124 @@
using AntDesign;
using Microsoft.AspNetCore.Components;
using NftFaucet.Components;
using NftFaucet.Constants;
using NftFaucet.Models.Enums;
namespace NftFaucet.Pages;
public class Step1Component : BasicComponent
{
protected string NameErrorMessage { get; set; }
protected string DescriptionErrorMessage { get; set; }
protected string ImageErrorMessage { get; set; }
protected string NameClass => string.IsNullOrWhiteSpace(NameErrorMessage) ? null : "invalid-input";
protected string DescriptionClass => string.IsNullOrWhiteSpace(DescriptionErrorMessage) ? null : "invalid-input";
protected string ImageClass => "file-uploader" + (string.IsNullOrWhiteSpace(ImageErrorMessage) ? string.Empty : " invalid-input");
protected EnumWrapper<IpfsGatewayType>[] IpfsGateways { get; } = Enum.GetValues<IpfsGatewayType>()
.Select(x => new EnumWrapper<IpfsGatewayType>(x, x.ToString())).ToArray();
protected EnumWrapper<TokenType>[] TokenTypes { get; } = Enum.GetValues<TokenType>()
.Select(x => new EnumWrapper<TokenType>(x, x.ToString())).ToArray();
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady())
UriHelper.NavigateTo("/");
AppState.Navigation.SetForwardHandler(ForwardHandler);
}
protected Task<bool> ForwardHandler()
{
var isValidName = !string.IsNullOrWhiteSpace(AppState.Storage.TokenName);
var isValidDescription = !string.IsNullOrWhiteSpace(AppState.Storage.TokenDescription);
var isValidFile = AppState.Storage.IpfsImageUrl != null;
var isNotUploading = !AppState.Storage.UploadIsInProgress;
if (!isValidName)
{
NameErrorMessage = "Invalid name";
}
if (!isValidDescription)
{
DescriptionErrorMessage = "Invalid description";
}
if (!isValidFile)
{
ImageErrorMessage = "Invalid file";
}
if (!isNotUploading)
{
ImageErrorMessage = "Upload is still in progress";
}
RefreshMediator.NotifyStateHasChangedSafe();
var canProceed = isValidName && isValidDescription && isValidFile && isNotUploading;
return Task.FromResult(canProceed);
}
protected void OnNameInputChange(ChangeEventArgs args)
{
NameErrorMessage = string.Empty;
}
protected void OnDescriptionInputChange(ChangeEventArgs args)
{
DescriptionErrorMessage = string.Empty;
}
protected void OnIpfsGatewayChange(EnumWrapper<IpfsGatewayType> ipfsGatewayItem)
{
AppState.Storage.IpfsGatewayType = ipfsGatewayItem.Value;
}
protected void OnTokenTypeChange(EnumWrapper<TokenType> tokenTypeItem)
{
AppState.Storage.TokenType = tokenTypeItem.Value;
if (AppState.Storage.TokenType == TokenType.ERC721)
{
AppState.Storage.TokenAmount = 1;
}
RefreshMediator.NotifyStateHasChangedSafe();
}
protected async Task<bool> BeforeUpload(List<UploadFileItem> files)
{
var file = files.FirstOrDefault();
if (file == null)
{
return false;
}
var hasValidSize = file.Size < UploadConstants.MaxFileSizeInBytes;
if (!hasValidSize)
{
MessageService.Error($"File must be smaller than {UploadConstants.MaxFileSizeInMegabytes} MB!");
return false;
}
ImageErrorMessage = string.Empty;
AppState.Storage.UploadIsInProgress = true;
RefreshMediator.NotifyStateHasChangedSafe();
AppState.Storage.IpfsImageUrl = await IpfsService.Upload(file.FileName, file.Type, file.ObjectURL);
AppState.Storage.LocalImageUrl = new Uri(file.ObjectURL);
ImageErrorMessage = string.Empty;
AppState.Storage.UploadIsInProgress = false;
AppState.Storage.CanPreviewTokenFile = file.IsPicture();
if (!AppState.Storage.CanPreviewTokenFile)
{
MessageService.Warning("Can't preview this file. Tho you can still mint a NFT with it.");
}
RefreshMediator.NotifyStateHasChangedSafe();
return false;
}
}

View File

@ -0,0 +1,13 @@
@page "/step2"
@inherits Step2Component
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token metadata:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@EditorClass">
<MonacoEditor @ref="Editor" Id="metadata-monaco-editor" ConstructionOptions="EditorConstructionOptions"/>
</div>
</SpaceItem>
</Space>

View File

@ -0,0 +1,82 @@
using System.Text;
using BlazorMonaco;
using Newtonsoft.Json;
using NftFaucet.Components;
using NftFaucet.Extensions;
using NftFaucet.Models.Token;
namespace NftFaucet.Pages;
public class Step2Component : BasicComponent
{
protected MonacoEditor Editor { get; set; }
protected string EditorErrorMessage { get; set; }
protected string EditorClass => string.IsNullOrWhiteSpace(EditorErrorMessage) ? null : "invalid-input";
protected StandaloneEditorConstructionOptions EditorConstructionOptions(MonacoEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "json",
GlyphMargin = true,
Value = GetCurrentMetadataJson(),
};
}
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady() || AppState.Storage.IpfsImageUrl == null)
UriHelper.NavigateTo("/");
AppState.Navigation.SetForwardHandler(ForwardHandler);
await Task.Yield();
await Editor.SetValue(GetCurrentMetadataJson());
}
protected async Task<bool> ForwardHandler()
{
var metadataJson = await Editor.GetValue();
if (!metadataJson.IsValidJson())
{
EditorErrorMessage = "Invalid JSON";
RefreshMediator.NotifyStateHasChangedSafe();
return false;
}
if (AppState.Storage.TokenMetadata == metadataJson)
{
return true;
}
AppState.Storage.UploadIsInProgress = true;
RefreshMediator.NotifyStateHasChangedSafe();
var metadataBytes = Encoding.UTF8.GetBytes(metadataJson);
var tokenUri = await IpfsService.Upload("token.json", "application/json", metadataBytes);
tokenUri = IpfsService.GetUrlToGateway(tokenUri, AppState.Storage.IpfsGatewayType);
AppState.Storage.TokenMetadata = metadataJson;
AppState.Storage.TokenUrl = tokenUri.OriginalString;
AppState.Storage.UploadIsInProgress = false;
RefreshMediator.NotifyStateHasChangedSafe();
return true;
}
private string GetCurrentMetadataJson()
{
var imageUrl = IpfsService.GetUrlToGateway(AppState.Storage.IpfsImageUrl, AppState.Storage.IpfsGatewayType);
var metadata = new TokenMetadata
{
Name = AppState.Storage.TokenName,
Description = AppState.Storage.TokenDescription,
Image = imageUrl.OriginalString,
ExternalUrl = "https://nft-faucet.darkcodi.xyz/",
};
var metadataJson = JsonConvert.SerializeObject(metadata, Formatting.Indented);
return metadataJson;
}
}

View File

@ -0,0 +1,21 @@
@page "/step3"
@inherits Step3Component
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token URI:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@TokenUrlClass">
<Input Size="medium" @bind-Value="@AppState.Storage.TokenUrl" OnInput="@OnTokenUrlInputChange" />
</div>
</SpaceItem>
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Destination address:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@DestinationAddressClass">
<Input Size="medium" @bind-Value="@AppState.Storage.DestinationAddress" OnInput="@OnDestinationAddressInputChange" />
</div>
</SpaceItem>
</Space>

View File

@ -0,0 +1,51 @@
using NftFaucet.Components;
using NftFaucet.Models;
namespace NftFaucet.Pages;
public class Step3Component : BasicComponent
{
protected string TokenUrlErrorMessage { get; set; }
protected string DestinationAddressErrorMessage { get; set; }
protected string TokenUrlClass => string.IsNullOrWhiteSpace(TokenUrlErrorMessage) ? null : "invalid-input";
protected string DestinationAddressClass => string.IsNullOrWhiteSpace(DestinationAddressErrorMessage) ? null : "invalid-input";
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady() || string.IsNullOrEmpty(AppState.Storage.TokenUrl))
UriHelper.NavigateTo("/");
AppState.Navigation.SetForwardHandler(ForwardHandler);
AppState.Storage.DestinationAddress = AppState.Metamask.Address;
}
protected void OnTokenUrlInputChange()
{
TokenUrlErrorMessage = string.Empty;
}
protected void OnDestinationAddressInputChange()
{
DestinationAddressErrorMessage = string.Empty;
}
protected Task<bool> ForwardHandler()
{
var isValidTokenUri = !string.IsNullOrWhiteSpace(AppState.Storage.TokenUrl);
var isValidDestinationAddress = Address.Create(AppState.Storage.DestinationAddress).IsSuccess;
if (!isValidTokenUri)
{
TokenUrlErrorMessage = "Invalid token URI";
}
if (!isValidDestinationAddress)
{
DestinationAddressErrorMessage = "Invalid destination address";
}
RefreshMediator.NotifyStateHasChangedSafe();
return Task.FromResult(isValidTokenUri && isValidDestinationAddress);
}
}

View File

@ -0,0 +1,17 @@
@page "/step4"
@inherits Step4Component
@if (TransactionHash != null)
{
<Result Status="success"
Title="Transaction for token minting was successfully created!"
SubTitle="@($"Transaction: {TransactionHash}. Please wait for 1-5 minutes till transaction is completed.")">
<Extra>
<Button Type="primary" OnClick="@Reset">Start again</Button>
</Extra>
</Result>
}
else
{
<TextWithIcon Text="Minting is in progress... Please wait." Icon="loading-3-quarters" Color="blue" Level="4" />
}

View File

@ -0,0 +1,51 @@
using System.Text;
using Microsoft.AspNetCore.Components;
using NftFaucet.Components;
using NftFaucet.Models.Enums;
using NftFaucet.Services;
namespace NftFaucet.Pages;
public class Step4Component : BasicComponent
{
[Inject]
public IIpfsService IpfsService { get; set; }
[Inject]
public IEthereumTransactionService TransactionService { get; set; }
protected string TransactionHash { get; set; }
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady() || string.IsNullOrEmpty(AppState.Storage.DestinationAddress))
{
UriHelper.NavigateTo("/");
}
else
{
Task.Run(Mint);
}
}
public async Task Mint()
{
if (AppState.Storage.TokenType == TokenType.ERC721)
{
TransactionHash = await TransactionService.MintErc721Token(AppState.Metamask.Network!.Value, AppState.Storage.DestinationAddress, AppState.Storage.TokenUrl);
}
else
{
var amount = (int) AppState.Storage.TokenAmount;
TransactionHash = await TransactionService.MintErc1155Token(AppState.Metamask.Network!.Value, AppState.Storage.DestinationAddress, amount, AppState.Storage.TokenUrl);
}
RefreshMediator.NotifyStateHasChangedSafe();
}
protected void Reset()
{
AppState.Reset();
UriHelper.NavigateTo("/");
}
}

View File

@ -2,12 +2,35 @@ using MetaMask.Blazor;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using NftFaucet;
using NftFaucet.ApiClients.NftStorage;
using NftFaucet.Models;
using NftFaucet.Options;
using NftFaucet.Services;
using RestEase;
using Serilog;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var settings = new Settings();
builder.Configuration.Bind(settings);
builder.Services.AddSingleton(settings);
Log.Logger = new LoggerConfiguration()
.WriteTo.BrowserConsole()
.CreateLogger();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)});
builder.Services.AddScoped(_ => new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)});
builder.Services.AddScoped<ScopedAppState>();
builder.Services.AddScoped<RefreshMediator>();
builder.Services.AddScoped<MetamaskInfo>();
builder.Services.AddScoped<NavigationWrapper>();
builder.Services.AddScoped<IEthereumTransactionService, EthereumTransactionService>();
builder.Services.AddScoped<IIpfsService, IpfsService>();
builder.Services.AddSingleton(_ => RestClient.For<INftStorageClient>());
builder.Services.AddAntDesign();
builder.Services.AddMetaMaskBlazor();

View File

@ -0,0 +1,46 @@
using System.Numerics;
using NftFaucet.Models;
using NftFaucet.Models.Enums;
using NftFaucet.Models.Function;
using NftFaucet.Options;
namespace NftFaucet.Services;
public class EthereumTransactionService : IEthereumTransactionService
{
private readonly Settings _settings;
private readonly MetamaskInfo _metamaskInfo;
public EthereumTransactionService(Settings settings, MetamaskInfo metamaskInfo)
{
_settings = settings;
_metamaskInfo = metamaskInfo;
}
public async Task<string> MintErc721Token(EthereumNetwork network, string destinationAddress, string tokenUri)
{
var options = _settings.GetEthereumNetworkOptions(network);
var transfer = new Erc721MintFunction
{
To = destinationAddress,
Uri = tokenUri,
};
var data = transfer.Encode();
var transactionHash = await _metamaskInfo.Service.SendTransaction(options.Erc721ContractAddress, 0, data);
return transactionHash;
}
public async Task<string> MintErc1155Token(EthereumNetwork network, string destinationAddress, BigInteger amount, string tokenUri)
{
var options = _settings.GetEthereumNetworkOptions(network);
var transfer = new Erc1155MintFunction
{
To = destinationAddress,
Amount = amount,
Uri = tokenUri,
};
var data = transfer.Encode();
var transactionHash = await _metamaskInfo.Service.SendTransaction(options.Erc1155ContractAddress, 0, data);
return transactionHash;
}
}

View File

@ -0,0 +1,10 @@
using System.Numerics;
using NftFaucet.Models.Enums;
namespace NftFaucet.Services;
public interface IEthereumTransactionService
{
Task<string> MintErc721Token(EthereumNetwork network, string destinationAddress, string tokenUri);
Task<string> MintErc1155Token(EthereumNetwork network, string destinationAddress, BigInteger amount, string tokenUri);
}

View File

@ -0,0 +1,10 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Services;
public interface IIpfsService
{
Uri GetUrlToGateway(Uri ipfsUrl, IpfsGatewayType gateway);
Task<Uri> Upload(string fileName, string fileType, string url);
Task<Uri> Upload(string fileName, string fileType, byte[] fileBytes);
}

View File

@ -0,0 +1,66 @@
using Newtonsoft.Json;
using NftFaucet.ApiClients.NftStorage;
using NftFaucet.Models.Enums;
using NftFaucet.Options;
using Serilog;
namespace NftFaucet.Services;
public class IpfsService : IIpfsService
{
private const string IpfsUrlPrefix = "ipfs://";
private readonly INftStorageClient _nftStorage;
private readonly Settings _settings;
public IpfsService(INftStorageClient nftStorage, Settings settings)
{
_nftStorage = nftStorage;
_settings = settings;
}
public Uri GetUrlToGateway(Uri ipfsUrl, IpfsGatewayType gateway)
{
if (ipfsUrl == null)
throw new ArgumentNullException(nameof(ipfsUrl));
if (!ipfsUrl.OriginalString.StartsWith(IpfsUrlPrefix, StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException(nameof(ipfsUrl));
if (gateway == IpfsGatewayType.None)
return ipfsUrl;
var options = _settings.GetIpfsGatewayOptions(gateway);
var prefix = options.BaseUrl;
if (!prefix.EndsWith("/"))
prefix += "/";
return new Uri(prefix + ipfsUrl.OriginalString.Replace(IpfsUrlPrefix, string.Empty));
}
public async Task<Uri> Upload(string fileName, string fileType, string url)
{
using var httpClient = new HttpClient();
var fileBytes = await httpClient.GetByteArrayAsync(new Uri(url));
return await Upload(fileName, fileType, fileBytes);
}
public async Task<Uri> Upload(string fileName, string fileType, byte[] fileBytes)
{
var fileUploadRequest = ToMultipartContent(fileName, fileType, fileBytes);
var response = await _nftStorage.UploadFile(fileUploadRequest);
Log.Information(JsonConvert.SerializeObject(response));
var uri = IpfsUrlPrefix + response.Value.Cid + "/" + response.Value.Files.First().Name;
return new Uri(uri);
}
private MultipartContent ToMultipartContent(string fileName, string fileType, byte[] bytes)
{
var content = new MultipartFormDataContent();
var imageContent = new ByteArrayContent(bytes);
imageContent.Headers.Add("Content-Type", fileType);
content.Add(imageContent, "\"file\"", $"\"{fileName}\"");
return content;
}
}

View File

@ -0,0 +1,3 @@
@inherits LayoutComponentBase
@Body

View File

@ -1,17 +1,26 @@
@inherits LayoutComponentBase
@inherits MainLayoutComponent
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<article class="content px-4">
<Layout Style="background-color: #FFFFFF; height: 100%;">
<Content Style="padding: 0 50px; display: flex; flex-direction: column;">
<PageHeader Title="NFT faucet" Subtitle="@Subtitle" Class="create-nft-header" />
<Steps Current="@(AppState.Navigation.CurrentStep - 1)" Type="navigation" Class="fix-alignment">
<Step Title="Specify token metadata"/>
<Step Title="Review metadata details"/>
<Step Title="Review mint details"/>
<Step Title="Wait for token minting"/>
</Steps>
<div style="flex-grow: 3;">
@Body
</article>
</main>
</div>
</div>
<Affix OffsetBottom="10">
<div style="display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: space-between; align-items: center;">
<Button Type="@ButtonType.Default" Size="large" Icon="arrow-left" Style="@BackButtonStyle" OnClick="@AppState.Navigation.GoBack">
Back
</Button>
<Button Type="@ButtonType.Primary" Size="large" Icon="@ForwardButtonIcon" Style="@ForwardButtonStyle" OnClick="@AppState.Navigation.GoForward" Loading="@AppState.Storage.UploadIsInProgress">
@ForwardButtonText
</Button>
</div>
</Affix>
</Content>
</Layout>

View File

@ -0,0 +1,29 @@
using NftFaucet.Components;
namespace NftFaucet.Shared;
public class MainLayoutComponent : LayoutBasicComponent
{
private const int StepsCount = 4;
protected string Subtitle => $"Address: {AppState?.Metamask?.Address ?? "<null>"}; Chain: {AppState?.Metamask?.Network?.ToString() ?? "<unknown>"} ({AppState?.Metamask?.ChainId.ToString() ?? "<null>"})";
protected bool IsFirstStep => AppState.Navigation.CurrentStep == 1;
protected bool IsLastStep => AppState.Navigation.CurrentStep == StepsCount;
protected string BackButtonStyle => $"visibility: {(IsFirstStep || IsLastStep ? "hidden" : "visible")}";
protected string ForwardButtonStyle => $"visibility: {(IsLastStep ? "hidden" : "visible")}";
protected string ForwardButtonText => AppState.Navigation.CurrentStep switch
{
1 => "Review NFT",
2 => "Review mint",
3 => "Send me this NFT!",
_ => "Next"
};
protected string ForwardButtonIcon => AppState.Navigation.CurrentStep switch
{
3 => "send",
_ => "arrow-right",
};
}

View File

@ -4,7 +4,7 @@
flex-direction: column;
}
main {
.main {
flex: 1;
}
@ -21,17 +21,12 @@ main {
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@ -45,7 +40,7 @@ main {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
.top-row a, .top-row .btn-link {
margin-left: 0;
}
}
@ -68,14 +63,18 @@ main {
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
.main > div {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
.create-nft-header {
padding-top: 0 !important;
padding-left: 0 !important;
padding-bottom: 0 !important;
}
.create-nft-header .ant-page-header-heading-title {
color: red;
}

View File

@ -1,40 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">NftFaucet</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@ -1,62 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
}

View File

@ -1,17 +0,0 @@
<div class="alert alert-secondary mt-4">
<span class="oi oi-pencil me-2" aria-hidden="true"></span>
<strong>@Title</strong>
<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2148851">brief survey</a>
</span>
and tell us what you think.
</div>
@code {
// Demonstrates how a parent component can supply parameters
[Parameter]
public string? Title { get; set; }
}

View File

@ -0,0 +1,90 @@
using CSharpFunctionalExtensions;
namespace NftFaucet.Utils;
public static class ResultWrapper
{
public static Result Wrap(Action action)
{
try
{
action();
return Result.Success();
}
catch (Exception e)
{
return Result.Failure(e.Message);
}
}
public static Result<T> Wrap<T>(Func<T> func)
{
try
{
return func();
}
catch (Exception e)
{
return Result.Failure<T>(e.Message);
}
}
public static async Task<Result> Wrap(Func<Task> func)
{
try
{
var task = func();
await task;
return Result.Success();
}
catch (Exception e)
{
return Result.Failure(e.Message);
}
}
public static async Task<Result<T>> Wrap<T>(Func<Task<T>> func)
{
try
{
var task = func();
return await task;
}
catch (Exception e)
{
return Result.Failure<T>(e.Message);
}
}
public static async ValueTask<Result> Wrap(Func<ValueTask> func)
{
try
{
var task = func();
await task;
return Result.Success();
}
catch (Exception e)
{
return Result.Failure(e.Message);
}
}
public static async ValueTask<Result<T>> Wrap<T>(Func<ValueTask<T>> func)
{
try
{
var task = func();
return await task;
}
catch (Exception e)
{
return Result.Failure<T>(e.Message);
}
}
public static Task<Result> Wrap(Task task) => Wrap(() => task);
public static Task<Result<T>> Wrap<T>(Task<T> task) => Wrap(() => task);
public static ValueTask<Result> Wrap(ValueTask task) => Wrap(() => task);
public static ValueTask<Result<T>> Wrap<T>(ValueTask<T> task) => Wrap(() => task);
}

View File

@ -8,4 +8,6 @@
@using Microsoft.JSInterop
@using NftFaucet
@using NftFaucet.Shared
@using NftFaucet.Components
@using AntDesign
@using BlazorMonaco

View File

@ -0,0 +1,28 @@
{
"EthereumNetworks": [
{
"Id": "Ropsten",
"Erc721ContractAddress": "0x64f27361c19b19f97cad58fefb9bccb27726668a",
"Erc1155ContractAddress": "0xEf69DFC7E8771bD1BE30a40f0f90FB0Ed98C699A"
},
{
"Id": "PolygonMumbai",
"Erc721ContractAddress": "0x42f166692662eCaAD8a5E758A2DB8fd7dBb31C24",
"Erc1155ContractAddress": "0x0742bd8cf4E3a1dd5F1340d52252b260552bf0eA"
}
],
"IpfsGateways": [
{
"Id": "IpfsOfficial",
"BaseUrl": "https://ipfs.io/ipfs"
},
{
"Id": "Infura",
"BaseUrl": "https://ipfs.infura.io/ipfs"
},
{
"Id": "NftStorage",
"BaseUrl": "https://nftstorage.link/ipfs"
}
]
}

View File

@ -46,12 +46,106 @@ a, .btn-link {
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.fix-alignment .ant-steps-icon {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
align-items: center !important;
justify-content: center !important;
}
.ant-btn > .anticon {
display: inline-flex !important;
}
.ant-form-item-control {
margin-left: 0 !important;
}
.file-uploader > .ant-upload {
width: 512px !important;
height: 512px !important;
}
.invalid-input input {
border-color: red !important;
}
.invalid-input textarea {
border-color: red !important;
}
.invalid-input.file-uploader > div {
border-color: red !important;
}
.invalid-input .monaco-editor-container {
border-color: red !important;
}
#app {
height: 100%;
width: 100%;
}
.drk-vertical-space {
height: 100% !important;
width: 100% !important;
}
.drk-vertical-space-center {
height: 100% !important;
width: 100% !important;
justify-content: center !important;
align-items: start !important;
}
.drk-dropdown-button {
width: 100% !important;
text-align: left !important;
}
.drk-full-width {
width: 100% !important;
}
.drk-full-height {
height: 100% !important;
}
.drk-grow {
flex-grow: 3 !important;
}
.drk-align-flex-start {
align-items: flex-start !important;
}
.ant-select-dropdown > div {
width: 100% !important;
}
.create-nft-header {
padding-top: 0 !important;
padding-left: 0 !important;
padding-bottom: 0 !important;
}
.create-nft-header .ant-page-header-heading-title {
color: red;
}
.monaco-editor-container {
height: 400px;
border: 1px solid gray;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
@ -59,6 +153,6 @@ a, .btn-link {
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.blazor-error-boundary::after {
content: "An error has occurred."
}

View File

@ -9,6 +9,7 @@
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="NftFaucet.styles.css" rel="stylesheet" />
<link href="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css" rel="stylesheet" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
</head>
@ -21,6 +22,10 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script>require.config({ paths: { 'vs': '_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="_content/BlazorMonaco/jsInterop.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>

View File

@ -1,27 +0,0 @@
[
{
"date": "2018-05-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2018-05-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2018-05-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2018-05-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2018-05-10",
"temperatureC": -2,
"summary": "Chilly"
}
]