mirror of
https://github.com/status-im/nft-faucet.git
synced 2025-02-20 18:48:13 +00:00
Squashed: Add prototype
This commit is contained in:
parent
3bc467e1d3
commit
69e5d18d3a
12
NftFaucet/ApiClients/NftStorage/INftStorageClient.cs
Normal file
12
NftFaucet/ApiClients/NftStorage/INftStorageClient.cs
Normal 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);
|
||||
}
|
21
NftFaucet/ApiClients/NftStorage/Models/UploadResponse.cs
Normal file
21
NftFaucet/ApiClients/NftStorage/Models/UploadResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
15
NftFaucet/Attributes/FunctionHashAttribute.cs
Normal file
15
NftFaucet/Attributes/FunctionHashAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
43
NftFaucet/Components/BasicComponent.cs
Normal file
43
NftFaucet/Components/BasicComponent.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
33
NftFaucet/Components/LayoutBasicComponent.cs
Normal file
33
NftFaucet/Components/LayoutBasicComponent.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
37
NftFaucet/Components/TextWithIcon.razor
Normal file
37
NftFaucet/Components/TextWithIcon.razor
Normal 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>
|
4
NftFaucet/Components/TextWithIcon.razor.css
Normal file
4
NftFaucet/Components/TextWithIcon.razor.css
Normal file
@ -0,0 +1,4 @@
|
||||
.space-item {
|
||||
align-items: center !important;
|
||||
display: flex !important;
|
||||
}
|
7
NftFaucet/Constants/UploadConstants.cs
Normal file
7
NftFaucet/Constants/UploadConstants.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace NftFaucet.Constants;
|
||||
|
||||
public static class UploadConstants
|
||||
{
|
||||
public const int MaxFileSizeInMegabytes = 20;
|
||||
public const long MaxFileSizeInBytes = MaxFileSizeInMegabytes * 1024 * 1024;
|
||||
}
|
2273
NftFaucet/Contracts/Erc1155Faucet_flat.sol
Normal file
2273
NftFaucet/Contracts/Erc1155Faucet_flat.sol
Normal file
File diff suppressed because it is too large
Load Diff
54
NftFaucet/Contracts/NftFaucet.sol
Normal file
54
NftFaucet/Contracts/NftFaucet.sol
Normal 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);
|
||||
}
|
||||
}
|
1479
NftFaucet/Contracts/NftFaucet_flat.sol
Normal file
1479
NftFaucet/Contracts/NftFaucet_flat.sol
Normal file
File diff suppressed because it is too large
Load Diff
27
NftFaucet/Extensions/StringExtensions.cs
Normal file
27
NftFaucet/Extensions/StringExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
14
NftFaucet/Extensions/TypeExtensions.cs
Normal file
14
NftFaucet/Extensions/TypeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
46
NftFaucet/Models/Address.cs
Normal file
46
NftFaucet/Models/Address.cs
Normal 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);
|
||||
}
|
15
NftFaucet/Models/Enums/EnumWrapper.cs
Normal file
15
NftFaucet/Models/Enums/EnumWrapper.cs
Normal 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;
|
||||
}
|
||||
}
|
12
NftFaucet/Models/Enums/EthereumNetwork.cs
Normal file
12
NftFaucet/Models/Enums/EthereumNetwork.cs
Normal 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,
|
||||
}
|
9
NftFaucet/Models/Enums/IpfsGatewayType.cs
Normal file
9
NftFaucet/Models/Enums/IpfsGatewayType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace NftFaucet.Models.Enums;
|
||||
|
||||
public enum IpfsGatewayType : byte
|
||||
{
|
||||
None = 0,
|
||||
IpfsOfficial = 1,
|
||||
Infura = 2,
|
||||
NftStorage = 3,
|
||||
}
|
7
NftFaucet/Models/Enums/TokenType.cs
Normal file
7
NftFaucet/Models/Enums/TokenType.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace NftFaucet.Models.Enums;
|
||||
|
||||
public enum TokenType : byte
|
||||
{
|
||||
ERC721 = 0,
|
||||
ERC1155 = 1,
|
||||
}
|
18
NftFaucet/Models/Function/Erc1155MintFunction.cs
Normal file
18
NftFaucet/Models/Function/Erc1155MintFunction.cs
Normal 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; }
|
||||
}
|
14
NftFaucet/Models/Function/Erc721MintFunction.cs
Normal file
14
NftFaucet/Models/Function/Erc721MintFunction.cs
Normal 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; }
|
||||
}
|
20
NftFaucet/Models/Function/Function.cs
Normal file
20
NftFaucet/Models/Function/Function.cs
Normal 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;
|
||||
}
|
96
NftFaucet/Models/MetamaskInfo.cs
Normal file
96
NftFaucet/Models/MetamaskInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
79
NftFaucet/Models/NavigationWrapper.cs
Normal file
79
NftFaucet/Models/NavigationWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
19
NftFaucet/Models/RefreshMediator.cs
Normal file
19
NftFaucet/Models/RefreshMediator.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
19
NftFaucet/Models/ScopedAppState.cs
Normal file
19
NftFaucet/Models/ScopedAppState.cs
Normal 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();
|
||||
}
|
||||
}
|
19
NftFaucet/Models/StateStorage.cs
Normal file
19
NftFaucet/Models/StateStorage.cs
Normal 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; }
|
||||
}
|
18
NftFaucet/Models/Token/TokenMetadata.cs
Normal file
18
NftFaucet/Models/Token/TokenMetadata.cs
Normal 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; }
|
||||
}
|
@ -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>
|
||||
|
10
NftFaucet/Options/EthereumNetworkOptions.cs
Normal file
10
NftFaucet/Options/EthereumNetworkOptions.cs
Normal 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; }
|
||||
}
|
9
NftFaucet/Options/IpfsGatewayOptions.cs
Normal file
9
NftFaucet/Options/IpfsGatewayOptions.cs
Normal 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; }
|
||||
}
|
15
NftFaucet/Options/Settings.cs
Normal file
15
NftFaucet/Options/Settings.cs
Normal 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);
|
||||
}
|
7
NftFaucet/Pages/ConnectMetamask.razor
Normal file
7
NftFaucet/Pages/ConnectMetamask.razor
Normal 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>
|
24
NftFaucet/Pages/ConnectMetamask.razor.cs
Normal file
24
NftFaucet/Pages/ConnectMetamask.razor.cs
Normal 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("/");
|
||||
}
|
||||
}
|
||||
}
|
@ -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++;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
|
11
NftFaucet/Pages/Index.razor.cs
Normal file
11
NftFaucet/Pages/Index.razor.cs
Normal 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");
|
||||
}
|
||||
}
|
111
NftFaucet/Pages/Step1Page.razor
Normal file
111
NftFaucet/Pages/Step1Page.razor
Normal 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>
|
124
NftFaucet/Pages/Step1Page.razor.cs
Normal file
124
NftFaucet/Pages/Step1Page.razor.cs
Normal 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;
|
||||
}
|
||||
}
|
13
NftFaucet/Pages/Step2Page.razor
Normal file
13
NftFaucet/Pages/Step2Page.razor
Normal 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>
|
82
NftFaucet/Pages/Step2Page.razor.cs
Normal file
82
NftFaucet/Pages/Step2Page.razor.cs
Normal 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;
|
||||
}
|
||||
}
|
21
NftFaucet/Pages/Step3Page.razor
Normal file
21
NftFaucet/Pages/Step3Page.razor
Normal 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>
|
51
NftFaucet/Pages/Step3Page.razor.cs
Normal file
51
NftFaucet/Pages/Step3Page.razor.cs
Normal 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);
|
||||
}
|
||||
}
|
17
NftFaucet/Pages/Step4Page.razor
Normal file
17
NftFaucet/Pages/Step4Page.razor
Normal 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" />
|
||||
}
|
51
NftFaucet/Pages/Step4Page.razor.cs
Normal file
51
NftFaucet/Pages/Step4Page.razor.cs
Normal 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("/");
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
46
NftFaucet/Services/EthereumTransactionService.cs
Normal file
46
NftFaucet/Services/EthereumTransactionService.cs
Normal 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;
|
||||
}
|
||||
}
|
10
NftFaucet/Services/IEthereumTransactionService.cs
Normal file
10
NftFaucet/Services/IEthereumTransactionService.cs
Normal 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);
|
||||
}
|
10
NftFaucet/Services/IIpfsService.cs
Normal file
10
NftFaucet/Services/IIpfsService.cs
Normal 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);
|
||||
}
|
66
NftFaucet/Services/IpfsService.cs
Normal file
66
NftFaucet/Services/IpfsService.cs
Normal 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;
|
||||
}
|
||||
}
|
3
NftFaucet/Shared/EmptyLayout.razor
Normal file
3
NftFaucet/Shared/EmptyLayout.razor
Normal file
@ -0,0 +1,3 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
@ -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>
|
||||
|
29
NftFaucet/Shared/MainLayout.razor.cs
Normal file
29
NftFaucet/Shared/MainLayout.razor.cs
Normal 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",
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
}
|
90
NftFaucet/Utils/ResultWrapper.cs
Normal file
90
NftFaucet/Utils/ResultWrapper.cs
Normal 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);
|
||||
}
|
@ -8,4 +8,6 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using NftFaucet
|
||||
@using NftFaucet.Shared
|
||||
@using NftFaucet.Components
|
||||
@using AntDesign
|
||||
@using BlazorMonaco
|
||||
|
28
NftFaucet/wwwroot/appsettings.json
Normal file
28
NftFaucet/wwwroot/appsettings.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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."
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user