Merge pull request #7 from darkcodi/new-ui

New UI
This commit is contained in:
Darkcodi 2022-09-29 23:31:15 -05:00 committed by GitHub
commit dfadcdb446
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 4892 additions and 2448 deletions

View File

@ -1,6 +1,6 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet", "NftFaucet\NftFaucet.csproj", "{EBEB2AEE-0DC0-4F3D-9675-680D5BCD8A01}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NftFaucet", "NftFaucet\NftFaucet.csproj", "{E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -8,9 +8,9 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EBEB2AEE-0DC0-4F3D-9675-680D5BCD8A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBEB2AEE-0DC0-4F3D-9675-680D5BCD8A01}.Debug|Any CPU.Build.0 = Debug|Any CPU {E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBEB2AEE-0DC0-4F3D-9675-680D5BCD8A01}.Release|Any CPU.ActiveCfg = Release|Any CPU {E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBEB2AEE-0DC0-4F3D-9675-680D5BCD8A01}.Release|Any CPU.Build.0 = Release|Any CPU {E113DAEE-A1E4-4BE2-8CDA-6E06245A471A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,14 +0,0 @@
using NftFaucet.ApiClients.Models;
using RestEase;
namespace NftFaucet.ApiClients;
[BaseAddress("https://gw.crustapps.net")]
public interface ICrustUploadApiClient
{
[Header("Authorization")]
public string Auth { get; set; }
[Post("api/v0/add")]
Task<UploadResponse> UploadFile([Body] MultipartContent content, [Query("pin")] bool pin = true);
}

View File

@ -10,5 +10,3 @@
</LayoutView> </LayoutView>
</NotFound> </NotFound>
</Router> </Router>
<AntContainer />

View File

@ -1,15 +1,15 @@
using AntDesign;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using NftFaucet.Models; using NftFaucet.Models.State;
using NftFaucet.Options; using NftFaucet.Options;
using NftFaucet.Services; using NftFaucet.Services;
using Radzen;
namespace NftFaucet.Components; namespace NftFaucet.Components;
public abstract class BasicComponent : ComponentBase public abstract class BasicComponent : ComponentBase
{ {
[Inject] [Inject]
protected NavigationManager UriHelper { get; set; } protected NavigationManager NavigationManager { get; set; }
[Inject] [Inject]
protected ScopedAppState AppState { get; set; } protected ScopedAppState AppState { get; set; }
@ -17,16 +17,23 @@ public abstract class BasicComponent : ComponentBase
[Inject] [Inject]
protected RefreshMediator RefreshMediator { get; set; } protected RefreshMediator RefreshMediator { get; set; }
[Inject]
protected DialogService DialogService { get; set; }
[Inject]
protected TooltipService TooltipService { get; set; }
[Inject]
protected NotificationService NotificationService { get; set; }
[Inject]
protected ContextMenuService ContextMenuService { get; set; }
[Inject] [Inject]
protected Settings Settings { get; set; } protected Settings Settings { get; set; }
[Inject] [Inject]
protected MessageService MessageService { get; set; } protected StateRepository StateRepository { get; set; }
[Inject]
protected IIpfsService IpfsService { get; set; }
protected MetamaskInfo Metamask => AppState?.Metamask;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@ -44,4 +51,9 @@ public abstract class BasicComponent : ComponentBase
// ignored // ignored
} }
} }
protected async Task SaveAppState()
{
await StateRepository.SaveAppState(AppState);
}
} }

View File

@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using NftFaucet.Models; using NftFaucet.Models.State;
using NftFaucet.Services;
namespace NftFaucet.Components; namespace NftFaucet.Components;
public abstract class LayoutBasicComponent : LayoutComponentBase public abstract class BasicLayout : LayoutComponentBase
{ {
[Inject] [Inject]
protected NavigationManager UriHelper { get; set; } protected NavigationManager UriHelper { get; set; }

View File

@ -0,0 +1,74 @@
@inherits BasicComponent
<RadzenDataList WrapItems="true" Data="@Data" TItem="CardListItem" >
<Template Context="cardListItem">
<RadzenCard class=@("box"
+ (cardListItem.IsDisabled ? " box-disabled" : string.Empty)
+ (SelectedItems != null && SelectedItems.Contains(cardListItem.Id)
? (cardListItem.SelectionIcon == CardListItemSelectionIcon.Checkmark ? " box-checkmark" : " box-warning")
: string.Empty))
Style="width: 250px;" onclick="@(async () => await ToggleSelection(cardListItem))">
<div class="d-flex flex-row align-items-center">
@if (SelectedItems != null && SelectedItems.Contains(cardListItem.Id))
{
if (cardListItem.SelectionIcon == CardListItemSelectionIcon.Checkmark)
{
<CheckmarkIcon Style="position: absolute; top: 1em; right: 1em;"/>
}
else if (cardListItem.SelectionIcon == CardListItemSelectionIcon.Warning)
{
<WarningIcon Style="position: absolute; top: 1em; right: 1em;"/>
}
}
@if (!string.IsNullOrEmpty(cardListItem.ImageLocation))
{
<RadzenImage Path=@(cardListItem.ImageLocation) Class="float-left mr-3" Style="width: 80px; height: 80px; margin-right: 1em;"/>
}
<div style="width: 100%;">
@if (!string.IsNullOrEmpty(cardListItem.Header))
{
<h4 class="mb-0">@cardListItem.Header</h4>
}
@foreach (var property in cardListItem.Properties ?? Array.Empty<CardListItemProperty>())
{
if (!string.IsNullOrEmpty(property?.Value))
{
var namePart = property.Name != null ? $"{property.Name}: " : string.Empty;
var valueStyle = property.ValueColor != null ? $"color: {property.ValueColor}" : string.Empty;
<div style="font-size: .8em">
<text style="font-weight: bold;">
@namePart
</text>
<text style="@valueStyle">
@if (!string.IsNullOrEmpty(property.Link))
{
<RadzenLink Icon="open_in_new" Path="@property.Value" Text="@property.Value" Target="_blank"/>
}
else
{
@property.Value
}
</text>
</div>
}
}
<div>
@foreach (var badge in cardListItem.Badges ?? Array.Empty<CardListItemBadge>())
{
if (!string.IsNullOrEmpty(badge?.Text))
{
<RadzenBadge BadgeStyle="@badge.Style" IsPill="true" Text="@badge.Text"/>
}
}
</div>
@if (cardListItem.Configuration != null)
{
<RadzenButton Icon="build" Click="@(async () => await OpenItemConfigurationDialog(cardListItem))" ButtonStyle="@ButtonStyle.Secondary"
Disabled="SelectedItems == null || !SelectedItems.Contains(cardListItem.Id)"
Style="position: absolute; bottom: 1em; right: 1em;" />
}
</div>
</div>
</RadzenCard>
</Template>
</RadzenDataList>

View File

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Components;
using Radzen;
namespace NftFaucet.Components.CardList;
public partial class CardList : BasicComponent
{
[Parameter] public CardListItem[] Data { get; set; }
[Parameter] public Guid[] SelectedItems { get; set; }
[Parameter] public EventCallback<Guid[]> SelectedItemsChanged { get; set; }
[Parameter] public EventCallback<Guid[]> OnSelectedChange { get; set; }
[Parameter] public bool AllowMultipleSelection { get; set; }
[Parameter] public bool AllowUnselect { get; set; }
public async Task ToggleSelection(CardListItem item)
{
if (item.IsDisabled)
{
return;
}
var selectedItems = SelectedItems?.ToList() ?? new List<Guid>();
var isAlreadySelected = selectedItems.Contains(item.Id);
if (isAlreadySelected && AllowUnselect)
{
selectedItems.Remove(item.Id);
}
else if (!isAlreadySelected)
{
if (!AllowMultipleSelection)
{
selectedItems.Clear();
}
selectedItems.Add(item.Id);
}
SelectedItems = selectedItems.ToArray();
await SelectedItemsChanged.InvokeAsync(SelectedItems);
await OnSelectedChange.InvokeAsync(SelectedItems);
RefreshMediator.NotifyStateHasChangedSafe();
}
protected async Task OpenItemConfigurationDialog(CardListItem item)
{
var result = (bool?) await DialogService.OpenAsync<CardListItemConfigurationDialog>("Configuration",
new Dictionary<string, object>
{
{ "CardListItemId", item.Id },
{ "CardListItem", item },
},
new DialogOptions() {Width = "700px", Height = "570px", Resizable = true, Draggable = true});
if (result != null && result.Value)
{
RefreshMediator.NotifyStateHasChangedSafe();
}
}
}

View File

@ -0,0 +1,13 @@
namespace NftFaucet.Components.CardList;
public class CardListItem
{
public Guid Id { get; set; }
public string ImageLocation { get; set; }
public string Header { get; set; }
public bool IsDisabled { get; set; }
public CardListItemSelectionIcon SelectionIcon { get; set; }
public CardListItemProperty[] Properties { get; set; } = Array.Empty<CardListItemProperty>();
public CardListItemBadge[] Badges { get; set; } = Array.Empty<CardListItemBadge>();
public CardListItemConfiguration Configuration { get; set; }
}

View File

@ -0,0 +1,9 @@
using Radzen;
namespace NftFaucet.Components.CardList;
public class CardListItemBadge
{
public BadgeStyle Style { get; set; }
public string Text { get; set; }
}

View File

@ -0,0 +1,9 @@
using CSharpFunctionalExtensions;
namespace NftFaucet.Components.CardList;
public class CardListItemConfiguration
{
public CardListItemConfigurationObject[] Objects { get; set; }
public Func<CardListItemConfigurationObject[], Task<Result>> ConfigureAction { get; set; }
}

View File

@ -0,0 +1,34 @@
@page "/configuration/{CardListItemId}"
@inherits BasicComponent
<PageTitle>Configuration</PageTitle>
<form onsubmit="@OnSavePressed">
@foreach (var configurationObject in CardListItem.Configuration.Objects)
{
switch (configurationObject.Type)
{
case CardListItemConfigurationObjectType.Input:
{
<div class="mb-4">
<h4>@configurationObject.Name</h4>
<RadzenTextBox Placeholder=@configurationObject.Placeholder @bind-Value="@configurationObject.Value" Disabled="@configurationObject.IsDisabled" Class="w-100"/>
</div>
break;
}
case CardListItemConfigurationObjectType.Button:
{
<div class="mb-4">
<RadzenButton Text="@configurationObject.Name" Click="@(() => configurationObject.ClickAction())" Disabled="@configurationObject.IsDisabled" Class="w-100"/>
</div>
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@((args) => OnSavePressed())" Text="Save" Style="width: 120px"/>
</div>
</form>

View File

@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Components;
using Radzen;
namespace NftFaucet.Components.CardList;
public partial class CardListItemConfigurationDialog : BasicComponent
{
[Parameter] public Guid CardListItemId { get; set; }
[Parameter] public CardListItem CardListItem { get; set; }
private async Task OnSavePressed()
{
var result = await CardListItem.Configuration.ConfigureAction(CardListItem.Configuration.Objects);
if (result.IsFailure)
{
NotificationService.Notify(NotificationSeverity.Error, "Invalid configuration", result.Error);
return;
}
await CardListItem.Configuration.ConfigureAction(CardListItem.Configuration.Objects);
RefreshMediator.NotifyStateHasChangedSafe();
DialogService.Close((bool?)true);
}
}

View File

@ -0,0 +1,15 @@
namespace NftFaucet.Components.CardList;
public class CardListItemConfigurationObject
{
public CardListItemConfigurationObjectType Type { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public string Icon { get; set; }
public string Placeholder { get; set; }
public bool IsDisabled { get; set; }
// for type=Button only
public Action ClickAction { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Components.CardList;
public enum CardListItemConfigurationObjectType
{
Input,
Button,
}

View File

@ -0,0 +1,9 @@
namespace NftFaucet.Components.CardList;
public class CardListItemProperty
{
public string Name { get; set; }
public string Value { get; set; }
public string ValueColor { get; set; }
public string Link { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Components.CardList;
public enum CardListItemSelectionIcon
{
Checkmark,
Warning
}

View File

@ -0,0 +1,5 @@
<span class="checkmark" style=@Style>
<div class="checkmark_circle"></div>
<div class="checkmark_stem"></div>
<div class="checkmark_kick"></div>
</span>

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace NftFaucet.Components.CheckmarkIcon;
public partial class CheckmarkIcon
{
[Parameter] public string Style { get; set; }
}

View File

@ -1,37 +0,0 @@
@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

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

View File

@ -0,0 +1,3 @@
<div style="@Style">
<span style="color: #FBBD08; font-size: 2em; vertical-align: middle; line-height: 0.6; display: inline-block; width: 22px; height: 22px;">⚠</span>
</div>

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace NftFaucet.Components.WarningIcon;
public partial class WarningIcon
{
[Parameter] public string Style { get; set; }
}

View File

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

View File

@ -1,88 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts@4.5.0/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts@4.5.0/token/ERC1155/extensions/ERC1155Supply.sol";
import "@openzeppelin/contracts@4.5.0/utils/Context.sol";
import "@openzeppelin/contracts@4.5.0/utils/Counters.sol";
contract Erc1155Faucet is Context, AccessControlEnumerable, ERC1155Supply {
string internal nftName;
string internal nftSymbol;
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
mapping(uint256 => string) internal _uriDict;
constructor() ERC1155("https://token-cdn-domain/{id}.json")
{
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
nftName = "ERC-1155 Faucet";
nftSymbol = "FA1155";
}
function mint(
address to,
uint256 amount,
string calldata tokenUri)
public
virtual
{
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_mint(to, tokenId, amount, "0x1234");
_uriDict[tokenId] = tokenUri;
}
function uri(uint256 id)
public
view
virtual
override
returns (string memory)
{
return _uriDict[id];
}
function name()
external
view
returns (string memory _name)
{
_name = nftName;
}
function symbol()
external
view
returns (string memory _symbol)
{
_symbol = nftSymbol;
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC1155, AccessControlEnumerable)
returns (bool)
{
return ERC1155.supportsInterface(interfaceId) || AccessControlEnumerable.supportsInterface(interfaceId);
}
function totalSupply()
public
view
returns (uint256)
{
uint256 result = 0;
uint256 maxId = _tokenIdCounter.current();
for(uint256 i = 0; i < maxId; i++)
{
result = result + totalSupply(i);
}
return result;
}
}

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
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 Erc721Faucet is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("ERC-721 Faucet", "FA721") {}
function safeMint(address to, string memory uri)
public
{
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
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 ERC721.supportsInterface(interfaceId) || ERC721Enumerable.supportsInterface(interfaceId);
}
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Extensions;
public static class EnumerableExtensions
{
public static IEnumerable<T> Duplicates<T>(this IEnumerable<T> source)
=> source.GroupBy(x => x).Where(kvp => kvp.Count() > 1).Select(kvp => kvp.Key);
}

View File

@ -1,27 +0,0 @@
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

@ -1,44 +0,0 @@
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;
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,7 @@
namespace NftFaucet.Models;
public class Balance
{
public decimal Amount { get; set; }
public string Currency { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace NftFaucet.Models.Dto;
public class AppStateDto
{
public Guid Id { get; set; } = Guid.Parse("621a252e-1d8d-4225-a045-b470469730cb");
public Guid? SelectedNetwork { get; set; }
public Guid? SelectedProvider { get; set; }
public Guid? SelectedContract { get; set; }
public Guid? SelectedToken { get; set; }
public Guid? SelectedUploadLocation { get; set; }
public string DestinationAddress { get; set; }
public int? TokenAmount { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Models.Dto;
public class ProviderStateDto
{
public Guid Id { get; set; }
public string State { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace NftFaucet.Models.Dto;
public class TokenDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; set; }
public string ImageFileName { get; set; }
public string ImageFileType { get; set; }
public string ImageFileData { get; set; }
public long? ImageFileSize { get; set; }
}

View File

@ -0,0 +1,11 @@
namespace NftFaucet.Models.Dto;
public class UploadLocationDto
{
public Guid Id { get; set; }
public Guid TokenId { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public DateTime CreatedAt { get; set; }
public Guid UploaderId { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace NftFaucet.Models.Dto;
public class UploaderStateDto
{
public Guid Id { get; set; }
public string State { get; set; }
}

View File

@ -1,15 +0,0 @@
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

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

View File

@ -1,30 +0,0 @@
namespace NftFaucet.Models.Enums;
public enum NetworkChain : long
{
EthereumMainnet = 1,
Ropsten = 3,
Rinkeby = 4,
Goerli = 5,
Kovan = 42,
OptimismMainnet = 10,
OptimismKovan = 69,
PolygonMainnet = 137,
PolygonMumbai = 80001,
MoonbeamMainnet = 1284,
MoonbaseAlpha = 1287,
ArbitrumMainnetBeta = 42161,
ArbitrumRinkeby = 421611,
ArbitrumGoerli = 421612,
AvalancheMainnet = 43114,
AvalancheFuji = 43113,
// BNB Smart Chain
BnbChainMainnet = 56,
BnbChainTestnet = 97,
// solana
SolanaMainnet = 11100,
SolanaDevnet = 11101,
SolanaTestnet = 11110,
}

View File

@ -1,6 +0,0 @@
namespace NftFaucet.Models.Enums;
public enum NetworkType
{
Ethereum,
Solana
}

View File

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

View File

@ -0,0 +1,35 @@
using Cryptography.ECDSA;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Util;
namespace NftFaucet.Models;
public class EthereumKey
{
public string PrivateKey { get; }
public string Address { get; }
public EthereumKey(string privateKey)
{
PrivateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey));
Address = GetAddressFromPrivateKey(privateKey);
}
public static EthereumKey GenerateNew()
{
var privateKeyBytes = Secp256K1Manager.GenerateRandomKey();
var privateKeyString = privateKeyBytes.ToHex(prefix: false);
return new EthereumKey(privateKeyString);
}
public static string GetAddressFromPrivateKey(string privateKey)
{
var privateKeyBytes = privateKey.HexToByteArray();
var publicKeyBytes = Secp256K1Manager.GetPublicKey(privateKeyBytes, false).Skip(1).ToArray();
var publicKeyHash = new Sha3Keccack().CalculateHash(publicKeyBytes);
var addressBytes = new byte[publicKeyHash.Length - 12];
Array.Copy(publicKeyHash, 12, addressBytes, 0, publicKeyHash.Length - 12);
var addressString = new AddressUtil().ConvertToChecksumAddress(addressBytes.ToHex());
return addressString;
}
}

View File

@ -1,21 +0,0 @@
namespace NftFaucet.Models;
public class IpfsBlockchainContext
{
public string Address { get; private set; }
public string SignedMessage { get; private set; }
public bool IsInitialized => !string.IsNullOrEmpty(Address) && !string.IsNullOrEmpty(SignedMessage);
public void Initialize(string address, string signedMessage)
{
if (string.IsNullOrEmpty(address))
throw new ArgumentNullException(nameof(address));
if (string.IsNullOrEmpty(signedMessage))
throw new ArgumentNullException(nameof(signedMessage));
Address = address;
SignedMessage = signedMessage;
}
}

View File

@ -1,101 +0,0 @@
using System.Globalization;
using System.Numerics;
using Ethereum.MetaMask.Blazor;
using Nethereum.Hex.HexTypes;
using NftFaucet.Models.Enums;
using NftFaucet.Services;
using NftFaucet.Utils;
using Serilog;
namespace NftFaucet.Models;
public class MetamaskInfo
{
private readonly RefreshMediator _refreshMediator;
public MetamaskInfo(IMetaMaskService service, MetamaskSigningService signingService, RefreshMediator refreshMediator)
{
Service = service;
SigningService = signingService;
_refreshMediator = refreshMediator;
}
public IMetaMaskService Service { get; }
public MetamaskSigningService SigningService { get; }
public bool? HasMetaMask { get; private set; }
public bool? IsMetaMaskConnected { get; private set; }
public string Address { get; private set; }
public BigInteger ChainId { get; private set; }
public NetworkChain? Network { get; private set; }
public async Task<bool> IsConnected()
{
HasMetaMask ??= await Service.IsMetaMaskAvailableAsync();
IsMetaMaskConnected ??= await Service.IsSiteConnectedAsync();
var isConnected = HasMetaMask.Value && IsMetaMaskConnected.Value;
return isConnected;
}
public async Task<bool> IsReady()
{
if (!await IsConnected())
return false;
if (string.IsNullOrEmpty(Address) || ChainId == 0)
{
await RefreshAddress();
SubscribeToEvents();
}
var isReady = HasMetaMask!.Value && IsMetaMaskConnected!.Value && ChainId != 0;
return isReady;
}
public async Task<bool> Connect()
{
var result = await ResultWrapper.Wrap(() => Service.ConnectAsync());
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.GetSelectedAccountAsync();
var chainIdHex = await Service.GetSelectedChainAsync();
ChainId = !string.IsNullOrEmpty(chainIdHex) ? new HexBigInteger(chainIdHex).Value : BigInteger.Zero;
Network = long.TryParse(ChainId.ToString(), out var longChainId) && Enum.IsDefined(typeof(NetworkChain), longChainId) ? (NetworkChain) longChainId : null;
_refreshMediator.NotifyStateHasChangedSafe();
}
private void SubscribeToEvents()
{
Service.AccountsChanged += OnAccountChangedEvent;
Service.ChainChanged += OnChainChangedEvent;
}
private void OnAccountChangedEvent(object sender, string[] args)
{
Address = args.FirstOrDefault();
_refreshMediator.NotifyStateHasChangedSafe();
}
private void OnChainChangedEvent(object sender, string chainIdHex)
{
ChainId = !string.IsNullOrEmpty(chainIdHex) ? new HexBigInteger(chainIdHex).Value : BigInteger.Zero;
Network = long.TryParse(ChainId.ToString(), out var longChainId) && Enum.IsDefined(typeof(NetworkChain), longChainId) ? (NetworkChain) longChainId : null;
_refreshMediator.NotifyStateHasChangedSafe();
}
}

View File

@ -0,0 +1,14 @@
using NftFaucet.Plugins;
using NftFaucet.Plugins.NetworkPlugins;
using NftFaucet.Plugins.ProviderPlugins;
namespace NftFaucet.Models;
public record MintRequest(
INetwork Network,
IProvider Provider,
IContract Contract,
IToken Token,
ITokenUploadLocation UploadLocation,
string DestinationAddress,
int TokensAmount);

View File

@ -0,0 +1,10 @@
namespace NftFaucet.Models;
public enum MintingState
{
CheckingNetwork,
CheckingAddress,
CheckingBalance,
SendingTransaction,
Done,
}

View File

@ -1,80 +0,0 @@
using Microsoft.AspNetCore.Components;
using NftFaucet.Extensions;
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.NavigateToRelative("/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.NavigateToRelative("/step" + nextStep);
}
}

View File

@ -0,0 +1,10 @@
namespace NftFaucet.Models;
public class NewFileModel
{
public string Name { get; set; }
public string Description { get; set; }
public string FileData { get; set; }
public string FileName { get; set; }
public long? FileSize { get; set; }
}

View File

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

View File

@ -1,36 +0,0 @@
using System.Text.RegularExpressions;
using CSharpFunctionalExtensions;
namespace NftFaucet.Models;
public class SolanaAddress : ValueObject<SolanaAddress>
{
private SolanaAddress(string value)
{
Value = value;
}
public string Value { get; }
public static implicit operator string(SolanaAddress address) => address.Value;
public static explicit operator SolanaAddress(string address) => Create(address).Value;
public static Result<SolanaAddress> Create(string value)
{
var regex = "^[1-9A-HJ-NP-Za-km-z]{32,44}$";
if (!Regex.IsMatch(value, regex))
{
return Result.Failure<SolanaAddress>("Invalid base58 string");
}
return new SolanaAddress(value);
}
public override string ToString() => Value;
protected override bool EqualsCore(SolanaAddress other)
=> string.Equals(Value, other.Value, StringComparison.InvariantCultureIgnoreCase);
protected override int GetHashCodeCore() => Value.GetHashCode(StringComparison.InvariantCultureIgnoreCase);
}

View File

@ -0,0 +1,39 @@
using Solnet.Wallet;
using Solnet.Wallet.Bip39;
namespace NftFaucet.Models;
public class SolanaKey
{
public string MnemonicPhrase { get; }
public string PrivateKey { get; }
public string Address { get; }
public SolanaKey(string mnemonicPhrase)
{
MnemonicPhrase = mnemonicPhrase ?? throw new ArgumentNullException(nameof(mnemonicPhrase));
PrivateKey = GetPrivateKeyFromMnemonicPhrase(mnemonicPhrase);
Address = GetAddressFromMnemonicPhrase(mnemonicPhrase);
}
public static SolanaKey GenerateNew()
{
var words = new Mnemonic(WordList.English, WordCount.Twelve).Words;
var mnemonicPhrase = string.Join(" ", words);
return new SolanaKey(mnemonicPhrase);
}
public static string GetPrivateKeyFromMnemonicPhrase(string mnemonicPhrase)
{
var mnemonic = new Mnemonic(mnemonicPhrase, WordList.English);
var wallet = new Wallet(mnemonic);
return wallet.Account.PrivateKey;
}
public static string GetAddressFromMnemonicPhrase(string mnemonicPhrase)
{
var mnemonic = new Mnemonic(mnemonicPhrase, WordList.English);
var wallet = new Wallet(mnemonic);
return wallet.Account.PublicKey.Key;
}
}

View File

@ -0,0 +1,13 @@
using NftFaucet.Plugins.NetworkPlugins;
using NftFaucet.Plugins.ProviderPlugins;
using NftFaucet.Plugins.UploadPlugins;
namespace NftFaucet.Models.State;
public class PluginStateStorage
{
public ICollection<INetwork> Networks { get; set; }
public ICollection<IProvider> Providers { get; set; }
public ICollection<IUploader> Uploaders { get; set; }
public ICollection<IContract> Contracts { get; set; }
}

View File

@ -0,0 +1,22 @@
using NftFaucet.Plugins;
using NftFaucet.Plugins.NetworkPlugins;
using NftFaucet.Plugins.ProviderPlugins;
namespace NftFaucet.Models.State;
public class ScopedAppState
{
public PluginStateStorage PluginStorage { get; private set; } = new();
public UserStateStorage UserStorage { get; private set; } = new();
public INetwork SelectedNetwork => PluginStorage?.Networks?.FirstOrDefault(x => x.Id == UserStorage?.SelectedNetworks?.FirstOrDefault());
public IProvider SelectedProvider => PluginStorage?.Providers?.FirstOrDefault(x => x.Id == UserStorage?.SelectedProviders?.FirstOrDefault());
public IContract SelectedContract => PluginStorage?.Contracts?.FirstOrDefault(x => x.Id == UserStorage?.SelectedContracts?.FirstOrDefault());
public IToken SelectedToken => UserStorage?.Tokens?.FirstOrDefault(x => x.Id == UserStorage?.SelectedTokens?.FirstOrDefault());
public ITokenUploadLocation SelectedUploadLocation => UserStorage?.UploadLocations?.FirstOrDefault(x => x.Id == UserStorage?.SelectedUploadLocations?.FirstOrDefault());
public void LoadUserStorage(UserStateStorage userStorage)
{
UserStorage = userStorage ?? new();
}
}

View File

@ -0,0 +1,17 @@
using NftFaucet.Plugins;
namespace NftFaucet.Models.State;
public class UserStateStorage
{
public ICollection<IToken> Tokens { get; set; }
public ICollection<ITokenUploadLocation> UploadLocations { get; set; }
public Guid[] SelectedNetworks { get; set; }
public Guid[] SelectedProviders { get; set; }
public Guid[] SelectedContracts { get; set; }
public Guid[] SelectedTokens { get; set; }
public Guid[] SelectedUploadLocations { get; set; }
public string DestinationAddress { get; set; }
public int TokenAmount { get; set; } = 1;
}

View File

@ -1,25 +0,0 @@
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.Crust;
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; }
public NetworkType NetworkType { get; set; }
public NetworkChain NetworkChain { get; set; }
public string TokenSymbol { get; set; } = "DFNT";
public bool IsTokenMutable { get; set; } = true;
public double SellerFeeBasisPoints { get; set; } = 88;
public bool IncludeMasterEdition { get; set; } = true;
}

View File

@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace NftFaucet.Models.Token; namespace NftFaucet.Models;
public class TokenMetadata public class TokenMetadata
{ {

View File

@ -2,27 +2,26 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AntDesign" Version="0.10.6-alpha.2" /> <PackageReference Include="ByteSize" Version="2.1.1" />
<PackageReference Include="BlazorMonaco" Version="2.1.0" /> <PackageReference Include="Cryptography.ECDSA.Secp256K1" Version="1.1.3" />
<PackageReference Include="CSharpFunctionalExtensions" Version="2.29.0" /> <PackageReference Include="CSharpFunctionalExtensions" Version="2.33.2" />
<PackageReference Include="Ethereum.MetaMask.Blazor" Version="1.0.0" /> <PackageReference Include="Ethereum.MetaMask.Blazor" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" /> <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="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
<PackageReference Include="Nethereum.ABI" Version="4.7.0" /> <PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nethereum.JsonRpc.Client" Version="4.7.0" /> <PackageReference Include="Nethereum.ABI" Version="4.8.0" />
<PackageReference Include="RestEase" Version="1.5.5" /> <PackageReference Include="Nethereum.JsonRpc.Client" Version="4.8.0" />
<PackageReference Include="Serilog.Sinks.BrowserConsole" Version="1.0.0" /> <PackageReference Include="Nethereum.Web3" Version="4.8.0" />
<PackageReference Include="Radzen.Blazor" Version="3.20.4" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="Solana.Metaplex" Version="1.2.0" /> <PackageReference Include="Solana.Metaplex" Version="1.2.0" />
<PackageReference Include="Toolbelt.Blazor.FileDropZone" Version="1.0.1" /> <PackageReference Include="TG.Blazor.IndexedDB" Version="1.5.0-preview" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -31,4 +30,8 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
</ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

@ -1,15 +1,8 @@
using NftFaucet.Models.Enums;
namespace NftFaucet.Options; namespace NftFaucet.Options;
public class Settings public class Settings
{ {
public NetworkOptions[] Networks { get; set; } public Guid[] RecommendedNetworks { get; set; }
public IpfsGatewayOptions[] IpfsGateways { get; set; } public Guid[] RecommendedProviders { get; set; }
public Guid[] RecommendedUploaders { get; set; }
public NetworkOptions GetEthereumNetworkOptions(NetworkChain network)
=> Networks.FirstOrDefault(x => x.Id == network);
public IpfsGatewayOptions GetIpfsGatewayOptions(IpfsGatewayType gateway)
=> IpfsGateways.FirstOrDefault(x => x.Id == gateway);
} }

View File

@ -1,19 +0,0 @@
@page "/connect-ipfs"
@layout EmptyLayout
@inherits ConnectIpfsComponent
<Space Align="center" Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem Class="drk-full-height">
<Space Align="center" Direction="DirectionVHType.Vertical" Class="drk-vertical-space-center" Style="align-items: center !important;">
<SpaceItem Style="height: 50%">
<img src="./images/crust.svg" alt="crust" style="height: 100%"/>
</SpaceItem>
<SpaceItem>
<Title Level="1">@TitleText</Title>
</SpaceItem>
<SpaceItem>
<Button Type="@ButtonType.Primary" Size="@ButtonSize.Large" OnClick="Sign">@ButtonText</Button>
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -1,43 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using NftFaucet.Components;
using NftFaucet.Extensions;
using NftFaucet.Utils;
namespace NftFaucet.Pages;
public class ConnectIpfsComponent : BasicComponent
{
[Inject]
protected IJSRuntime JsRuntime { get; set; }
protected string TitleText => "In order to use Crust (IPFS provider), please sign the message";
protected string ButtonText => "Sign message";
protected override async Task OnInitializedAsync()
{
if (await Metamask.IsConnected())
{
await Metamask.RefreshAddress();
}
if (AppState.IpfsContext.IsInitialized)
{
UriHelper.NavigateToRelative("/");
}
}
protected async Task Sign()
{
var address = Metamask.Address.ToLowerInvariant();
var signedMessageResult = await ResultWrapper.Wrap(Metamask.SigningService.SignAsync(address));
if (signedMessageResult.IsFailure)
{
return;
}
var signedMessage = signedMessageResult.Value;
AppState.IpfsContext.Initialize(address, signedMessage);
UriHelper.NavigateToRelative("/");
}
}

View File

@ -1,19 +0,0 @@
@page "/connect-metamask"
@layout EmptyLayout
@inherits ConnectMetamaskComponent
<Space Align="center" Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem Class="drk-full-height">
<Space Align="center" Direction="DirectionVHType.Vertical" Class="drk-vertical-space-center" Style="align-items: center !important;">
<SpaceItem Style="height: 50%">
<img src="./images/metamask_fox.svg" alt="metamask" style="height: 100%"/>
</SpaceItem>
<SpaceItem>
<Title Level="1">@TitleText</Title>
</SpaceItem>
<SpaceItem>
<Button Type="@ButtonType.Primary" Size="@ButtonSize.Large" OnClick="Connect">@ButtonText</Button>
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -1,43 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using NftFaucet.Components;
using NftFaucet.Extensions;
namespace NftFaucet.Pages;
public class ConnectMetamaskComponent : BasicComponent
{
[Inject]
protected IJSRuntime JsRuntime { get; set; }
protected bool HasMetaMask { get; set; }
protected string TitleText => $"You should {(HasMetaMask ? "connect" : "install")} MetaMask first";
protected string ButtonText => HasMetaMask ? "Connect" : "Install";
protected override async Task OnInitializedAsync()
{
if (await Metamask.IsConnected())
{
await Metamask.RefreshAddress();
UriHelper.NavigateToRelative("/");
}
HasMetaMask = Metamask.HasMetaMask ?? false;
}
protected async Task Connect()
{
if (!HasMetaMask)
{
string url = "https://metamask.io/download/";
await JsRuntime.InvokeAsync<object>("open", url, "_blank");
return;
}
var isConnected = await Metamask.Connect();
if (isConnected)
{
UriHelper.NavigateToRelative("/");
}
}
}

View File

@ -0,0 +1,19 @@
@page "/contracts"
@inherits BasicComponent
<PageTitle>Contracts</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select contract" />
@if (AppState.SelectedNetwork == null)
{
<RadzenHeading Size="H3" Text="Please choose network first!" />
}
else
{
<div style="width: 100%; display: flex; flex-direction: row; justify-content: end;">
<RadzenButton Text="Deploy New" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary"
Click="@(() => NotificationService.Notify(NotificationSeverity.Warning, "NOT IMPLEMENTED", "Will be implemented later"))" />
</div>
<CardList Data="@ContractCards" OnSelectedChange="@(async _ => await OnContractChange())" @bind-SelectedItems="@AppState.UserStorage.SelectedContracts" />
}
</RadzenContent>

View File

@ -0,0 +1,66 @@
using System.Globalization;
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Plugins.NetworkPlugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class ContractsPage : BasicComponent
{
protected override void OnInitialized()
{
Contracts = AppState.SelectedNetwork?.DeployedContracts?.ToArray() ?? Array.Empty<IContract>();
RefreshCards();
}
private IContract[] Contracts { get; set; }
private CardListItem[] ContractCards { get; set; }
private void RefreshCards()
{
ContractCards = Contracts.Select(MapCardListItem).ToArray();
}
private CardListItem MapCardListItem(IContract contract)
=> new CardListItem
{
Id = contract.Id,
Header = contract.Name,
Properties = new[]
{
new CardListItemProperty
{
Name = "Symbol",
Value = contract.Symbol,
},
new CardListItemProperty
{
Name = "Address",
Value = contract.Address,
},
new CardListItemProperty
{
Name = "TxHash",
Value = contract.DeploymentTxHash ?? "<unknown>",
},
new CardListItemProperty
{
Name = "DeployedAt",
Value = contract.DeployedAt?.ToString(CultureInfo.InvariantCulture) ?? "<unknown>",
},
},
Badges = new[]
{
contract.IsVerified
? new CardListItemBadge {Style = BadgeStyle.Success, Text = "Verified"}
: null,
new CardListItemBadge {Style = BadgeStyle.Light, Text = contract.Type.ToString()},
}.Where(x => x != null).ToArray(),
};
private async Task OnContractChange()
{
await SaveAppState();
}
}

View File

@ -0,0 +1,25 @@
@page "/tokens/new"
@inherits BasicComponent
<PageTitle>Create token</PageTitle>
<form onsubmit="@OnSavePressed">
<div class="mb-4">
<h4>Image</h4>
<RadzenFileInput @bind-Value=@Model.FileData @bind-FileName=@Model.FileName @bind-FileSize=@Model.FileSize TValue="string" Class="w-100"/>
</div>
<div class="mb-4">
<h4>Name</h4>
<RadzenTextBox Placeholder="YING #668" MaxLength="50" @bind-Value="@Model.Name" Class="w-100"/>
</div>
<div class="mb-4">
<h4>Description</h4>
<RadzenTextArea Placeholder="YING, ecotype enterprises IP of Inkeverse and the avatar spokesperson for Inkeverse." MaxLength="250" @bind-Value="@Model.Description" Class="w-100"/>
</div>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@((args) => OnSavePressed())" Disabled="!ModelIsValid" Text="Save" Style="width: 120px"/>
</div>
</form>

View File

@ -0,0 +1,65 @@
using MimeTypes;
using NftFaucet.Components;
using NftFaucet.Models;
using NftFaucet.Plugins;
namespace NftFaucet.Pages;
public partial class CreateTokenDialog : BasicComponent
{
private NewFileModel Model { get; set; } = new NewFileModel();
private bool ModelIsValid => IsValid();
private void OnSavePressed()
{
if (!IsValid())
return;
var token = new Token
{
Id = Guid.NewGuid(),
Name = Model.Name,
Description = Model.Description,
CreatedAt = DateTime.Now,
Image = new TokenMedia
{
FileName = Model.FileName,
FileType = DetermineFileType(Model.FileName),
FileData = Model.FileData,
FileSize = Model.FileSize!.Value,
},
};
DialogService.Close(token);
}
private string DetermineFileType(string fileName)
{
var extension = fileName.Split('.', StringSplitOptions.RemoveEmptyEntries).Last();
if (!MimeTypeMap.TryGetMimeType(extension, out var mimeType))
{
return "application/octet-stream";
}
return mimeType;
}
private bool IsValid()
{
if (string.IsNullOrWhiteSpace(Model.Name))
return false;
if (string.IsNullOrWhiteSpace(Model.Description))
return false;
if (string.IsNullOrEmpty(Model.FileData))
return false;
if (string.IsNullOrEmpty(Model.FileName))
return false;
if (Model.FileSize is null or 0)
return false;
return true;
}
}

View File

@ -0,0 +1,14 @@
@page "/uploads/new"
@inherits BasicComponent
<PageTitle>Create upload</PageTitle>
<RadzenContent Container="main">
<h3>Select uploader</h3>
<CardList Data="@UploaderCards" @bind-SelectedItems="@SelectedUploaderIds"/>
<div class="row">
<div class="col-md-12 text-right">
<RadzenButton Text="Cancel" Click="@(args => DialogService.Close())" ButtonStyle="ButtonStyle.Secondary" Disabled="IsUploading" Style="width: 120px" Class="mr-1"/>
<RadzenButton Text="Upload" Icon="eject" BusyText="Uploading..." IsBusy=@IsUploading Click="@(async args => await OnSavePressed())" Disabled="@(SelectedUploader == null || !SelectedUploader.IsConfigured)" Style="width: 180px"/>
</div>
</div>
</RadzenContent>

View File

@ -0,0 +1,132 @@
using System.Text;
using Microsoft.AspNetCore.Components;
using Newtonsoft.Json;
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Models;
using NftFaucet.Plugins;
using NftFaucet.Plugins.UploadPlugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class CreateUploadDialog : BasicComponent
{
[Parameter] public IToken Token { get; set; }
protected override void OnInitialized()
{
RefreshCards();
base.OnInitialized();
}
private CardListItem[] UploaderCards { get; set; }
private Guid[] SelectedUploaderIds { get; set; }
private IUploader SelectedUploader => AppState?.PluginStorage?.Uploaders?.FirstOrDefault(x => x.Id == SelectedUploaderIds?.FirstOrDefault());
private bool IsUploading { get; set; }
private void RefreshCards()
{
UploaderCards = AppState.PluginStorage.Uploaders.Select(MapCardListItem).ToArray();
}
private CardListItem MapCardListItem(IUploader uploader)
{
var configuration = uploader.GetConfiguration();
return new CardListItem
{
Id = uploader.Id,
ImageLocation = uploader.ImageName != null ? "./images/" + uploader.ImageName : null,
Header = uploader.Name,
Properties = uploader.GetProperties(),
IsDisabled = !uploader.IsSupported,
SelectionIcon = uploader.IsConfigured ? CardListItemSelectionIcon.Checkmark : CardListItemSelectionIcon.Warning,
Badges = new[]
{
(Settings?.RecommendedUploaders?.Contains(uploader.Id) ?? false)
? new CardListItemBadge {Style = BadgeStyle.Success, Text = "Recommended"}
: null,
!uploader.IsSupported
? new CardListItemBadge {Style = BadgeStyle.Light, Text = "Not Supported"}
: null,
}.Where(x => x != null).ToArray(),
Configuration = configuration == null
? null
: new CardListItemConfiguration
{
Objects = configuration.Objects,
ConfigureAction = async x =>
{
var result = await configuration.ConfigureAction(x);
RefreshCards();
if (result.IsSuccess)
{
await StateRepository.SaveUploaderState(uploader);
}
return result;
},
},
};
}
private async Task OnSavePressed()
{
IsUploading = true;
RefreshMediator.NotifyStateHasChangedSafe();
var imageLocationResult = await SelectedUploader.Upload(Token.Image.FileName, Token.Image.FileType, Base64DataToBytes(Token.Image.FileData));
if (imageLocationResult.IsFailure)
{
IsUploading = false;
RefreshMediator.NotifyStateHasChangedSafe();
NotificationService.Notify(NotificationSeverity.Error, "Uploading image failed", imageLocationResult.Error);
return;
}
var imageLocation = imageLocationResult.Value;
var tokenMetadata = GenerateTokenMetadata(Token, imageLocation);
var tokenMetadataBytes = Encoding.UTF8.GetBytes(tokenMetadata);
var tokenLocationResult = await SelectedUploader.Upload($"{Token.Id}.json", "application/json", tokenMetadataBytes);
if (tokenLocationResult.IsFailure)
{
IsUploading = false;
RefreshMediator.NotifyStateHasChangedSafe();
NotificationService.Notify(NotificationSeverity.Error, "Uploading metadata failed", tokenLocationResult.Error);
return;
}
IsUploading = false;
RefreshMediator.NotifyStateHasChangedSafe();
NotificationService.Notify(NotificationSeverity.Success, "Upload succeeded", tokenLocationResult.Value.OriginalString);
var uploadLocation = new TokenUploadLocation
{
Id = Guid.NewGuid(),
TokenId = Token.Id,
Name = SelectedUploader.ShortName,
Location = tokenLocationResult.Value.OriginalString,
CreatedAt = DateTime.Now,
UploaderId = SelectedUploader.Id,
};
DialogService.Close(uploadLocation);
}
private static byte[] Base64DataToBytes(string fileData)
{
var index = fileData.IndexOf(';');
var encoded = fileData.Substring(index + 8);
return Convert.FromBase64String(encoded);
}
private static string GenerateTokenMetadata(IToken token, Uri imageLocation)
{
var tokenMetadata = new TokenMetadata
{
Name = token.Name,
Description = token.Description,
Image = imageLocation.OriginalString,
ExternalUrl = "https://darkcodi.github.io/nft-faucet/",
};
var metadataJson = JsonConvert.SerializeObject(tokenMetadata, Formatting.Indented);
return metadataJson;
}
}

View File

@ -1,24 +0,0 @@
using NftFaucet.Components;
using NftFaucet.Extensions;
namespace NftFaucet.Pages;
public class IndexComponent : BasicComponent
{
protected override async Task OnInitializedAsync()
{
if (!await Metamask.IsReady())
{
UriHelper.NavigateToRelative("/connect-metamask");
return;
}
if (!AppState.IpfsContext.IsInitialized)
{
UriHelper.NavigateToRelative("/connect-ipfs");
return;
}
UriHelper.NavigateToRelative("/step1");
}
}

View File

@ -1,6 +1,6 @@
@page "/" @page "/"
@layout EmptyLayout @layout EmptyLayout
@inherits IndexComponent @inherits BasicComponent
<div> <div>
<div class="loading">Loading...</div> <div class="loading">Loading...</div>

View File

@ -0,0 +1,24 @@
using NftFaucet.Components;
using NftFaucet.Extensions;
namespace NftFaucet.Pages;
public partial class IndexPage : BasicComponent
{
protected override async Task OnInitializedAsync()
{
// if (!await Metamask.IsReady())
// {
// UriHelper.NavigateToRelative("/connect-metamask");
// return;
// }
//
// if (!AppState.IpfsContext.IsInitialized)
// {
// UriHelper.NavigateToRelative("/connect-ipfs");
// return;
// }
NavigationManager.NavigateToRelative("/networks");
}
}

View File

@ -0,0 +1,93 @@
@page "/mint/in-progress"
@using NftFaucet.Models
@inherits BasicComponent
<PageTitle>Minting...</PageTitle>
@if (!string.IsNullOrEmpty(ProgressBarText))
{
<RadzenText TextStyle="TextStyle.Subtitle2" TagName="TagName.H3">@ProgressBarText</RadzenText>
<RadzenProgressBar Value="100" ShowValue="false" Mode="ProgressBarMode.Indeterminate" />
}
else if (State == MintingState.CheckingNetwork)
{
<RadzenContent Container="main">
<h4>Network mismatch!</h4>
<p>
<text style="font-weight: bold;">Selected network: </text>
<text style="color: green;">@(AppState?.SelectedNetwork?.Name ?? "<unknown>")</text>
</p>
<p>
<text style="font-weight: bold;">Provider network: </text>
<text style="color: red;">@(ProviderNetwork?.Name ?? "<unknown>")</text>
</p>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@(async (args) => await CheckNetwork())" Text="Try again" Disabled="@(!string.IsNullOrEmpty(ProgressBarText))" />
</div>
</RadzenContent>
} else if (State == MintingState.CheckingAddress)
{
<RadzenContent Container="main">
<h3>Address is not available!</h3>
<p>
<text style="font-weight: bold;">Provider address: </text>
<text style="color: red;">&lt;null&gt;</text>
</p>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@(async (args) => await CheckNetwork())" Text="Try again" Disabled="@(!string.IsNullOrEmpty(ProgressBarText))" />
</div>
</RadzenContent>
} else if (State == MintingState.CheckingBalance)
{
<RadzenContent Container="main">
<h3>Not enough balance!</h3>
<p>
<text style="font-weight: bold;">Balance</text>
<text> (@SourceAddress)</text>
<text style="font-weight: bold;">: </text>
<text style="color: red;">
@if (Balance != null)
{
@(Balance.Amount + " " + Balance.Currency)
}
else
{
@:&lt;unknown&gt;
}
</text>
</p>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@(async (args) => await CheckNetwork())" Text="Try again" Disabled="@(!string.IsNullOrEmpty(ProgressBarText))" />
</div>
</RadzenContent>
} else if (State == MintingState.SendingTransaction)
{
<RadzenContent Container="main">
<h3>Failed to send transaction!</h3>
<p>
<text>Error message:</text>
<br/>
<text style="font-weight: bold; color: red;">@SendTransactionError</text>
</p>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@(async (args) => await CheckNetwork())" Text="Try again" Disabled="@(!string.IsNullOrEmpty(ProgressBarText))" />
</div>
</RadzenContent>
} else if (State == MintingState.Done)
{
<RadzenContent Container="main">
<h3>Success</h3>
<p>
<text style="font-weight: bold;">Token minted!</text>
<br/>
<br/>
<br/>
<text style="font-weight: bold;">Transaction hash: </text>
<br/>
<RadzenLink Icon="open_in_new" Path="@(new Uri(AppState.SelectedNetwork.ExplorerUrl, "tx/" + TransactionHash).ToString())" Text="@TransactionHash" Target="_blank"/>
</p>
<div style="display: flex; justify-content: end;">
<RadzenButton Click="@(async (args) => await Close())" Text="Close" Disabled="@(!string.IsNullOrEmpty(ProgressBarText))" />
</div>
</RadzenContent>
}

View File

@ -0,0 +1,146 @@
using NftFaucet.Components;
using NftFaucet.Models;
using NftFaucet.Plugins.NetworkPlugins;
using NftFaucet.Utils;
namespace NftFaucet.Pages;
public partial class MintDialog : BasicComponent
{
private const int MinDelayInMilliseconds = 300;
private string ProgressBarText { get; set; } = "Checking network...";
private MintingState State { get; set; } = MintingState.CheckingNetwork;
private INetwork ProviderNetwork { get; set; }
private string SourceAddress { get; set; }
private Balance Balance { get; set; }
private string TransactionHash { get; set; }
private string SendTransactionError { get; set; }
protected override Task OnInitializedAsync()
{
CheckNetwork();
return Task.CompletedTask;
}
private async Task CheckNetwork()
{
State = MintingState.CheckingNetwork;
ProgressBarText = "Checking network...";
RefreshMediator.NotifyStateHasChangedSafe();
var providerNetworkResult = await ResultWrapper.Wrap(async () =>
{
var task1 = AppState.SelectedProvider.GetNetwork(AppState.PluginStorage.Networks.ToArray(), AppState.SelectedNetwork);
var task2 = Task.Delay(TimeSpan.FromMilliseconds(MinDelayInMilliseconds));
await Task.WhenAll(task1, task2);
return task1.Result;
});
ProviderNetwork = providerNetworkResult.IsSuccess ? providerNetworkResult.Value : null;
if (ProviderNetwork?.Id == AppState.SelectedNetwork.Id)
{
CheckAddress();
}
else
{
ProgressBarText = null;
RefreshMediator.NotifyStateHasChangedSafe();
}
}
private async Task CheckAddress()
{
State = MintingState.CheckingAddress;
ProgressBarText = "Checking address...";
RefreshMediator.NotifyStateHasChangedSafe();
var sourceAddressResult = await ResultWrapper.Wrap(async () =>
{
var task1 = AppState.SelectedProvider.GetAddress();
var task2 = Task.Delay(TimeSpan.FromMilliseconds(MinDelayInMilliseconds));
await Task.WhenAll(task1, task2);
return task1.Result;
});
SourceAddress = sourceAddressResult.IsSuccess ? sourceAddressResult.Value : null;
if (!string.IsNullOrEmpty(SourceAddress))
{
CheckBalance();
}
else
{
ProgressBarText = null;
RefreshMediator.NotifyStateHasChangedSafe();
}
}
private async Task CheckBalance()
{
State = MintingState.CheckingBalance;
ProgressBarText = "Checking balance...";
RefreshMediator.NotifyStateHasChangedSafe();
var balanceResult = await ResultWrapper.Wrap(async () =>
{
var task1 = AppState.SelectedProvider.GetBalance(AppState.SelectedNetwork);
var task2 = Task.Delay(TimeSpan.FromMilliseconds(MinDelayInMilliseconds));
await Task.WhenAll(task1, task2);
return task1.Result;
});
Balance = balanceResult.IsSuccess ? balanceResult.Value : null;
var amount = Math.Max(Balance?.Amount ?? 0, 0);
if (amount != 0)
{
SendTransaction();
}
else
{
ProgressBarText = null;
RefreshMediator.NotifyStateHasChangedSafe();
}
}
private async Task SendTransaction()
{
State = MintingState.SendingTransaction;
ProgressBarText = "Sending transaction...";
RefreshMediator.NotifyStateHasChangedSafe();
var sendTransactionResult = await ResultWrapper.Wrap(async () =>
{
var mintRequest = new MintRequest(AppState.SelectedNetwork, AppState.SelectedProvider,
AppState.SelectedContract, AppState.SelectedToken, AppState.SelectedUploadLocation,
AppState.UserStorage.DestinationAddress, AppState.UserStorage.TokenAmount);
var task1 = AppState.SelectedProvider.Mint(mintRequest);
var task2 = Task.Delay(TimeSpan.FromMilliseconds(MinDelayInMilliseconds));
await Task.WhenAll(task1, task2);
return task1.Result;
});
if (sendTransactionResult.IsSuccess)
{
if (!string.IsNullOrEmpty(sendTransactionResult.Value))
{
TransactionHash = sendTransactionResult.Value;
State = MintingState.Done;
}
else
{
SendTransactionError = "Tx hash is null or empty";
}
}
else
{
SendTransactionError = sendTransactionResult.Error;
}
ProgressBarText = null;
RefreshMediator.NotifyStateHasChangedSafe();
}
private async Task Close()
{
DialogService.Close(TransactionHash);
}
}

View File

@ -0,0 +1,82 @@
@page "/mint"
@using NftFaucet.Plugins.NetworkPlugins
@inherits BasicComponent
<PageTitle>Mint</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Mint" />
<p>
<text style="font-weight: bold;">Network: </text>
@if (AppState?.SelectedNetwork == null)
{
@:<text style="color: red;">NOT SELECTED</text>
}
else
{
@:<text style="color: green;">OK</text>
}
</p>
<p>
<text style="font-weight: bold;">Provider: </text>
@if (AppState?.SelectedProvider == null)
{
@:<text style="color: red;">NOT SELECTED</text>
}
else if (!AppState.SelectedProvider.IsConfigured)
{
@:<text style="color: red;">NOT CONFIGURED</text>
}
else
{
@:<text style="color: green;">OK</text>
}
</p>
<p>
<text style="font-weight: bold;">Contract: </text>
@if (AppState?.SelectedContract == null)
{
@:<text style="color: red;">NOT SELECTED</text>
}
else
{
@:<text style="color: green;">OK</text>
}
</p>
<p>
<text style="font-weight: bold;">Token: </text>
@if (AppState?.SelectedToken == null)
{
@:<text style="color: red;">NOT SELECTED</text>
}
else
{
@:<text style="color: green;">OK</text>
}
</p>
<p>
<text style="font-weight: bold;">Upload location: </text>
@if (AppState?.SelectedUploadLocation == null)
{
@:<text style="color: red;">NOT SELECTED</text>
}
else
{
@:<text style="color: green;">OK</text>
}
</p>
@if (IsReadyToMint)
{
<div class="mb-4">
<h4>Destination address</h4>
<RadzenTextBox Placeholder="<null>" @bind-Value="@AppState.UserStorage.DestinationAddress" Class="w-100"/>
</div>
if (AppState!.SelectedContract!.Type == ContractType.Erc1155)
{
<div class="mb-4">
<h4>Tokens amount</h4>
<RadzenNumeric TValue="int" Min="1" Max="100000" @bind-Value="@AppState.UserStorage.TokenAmount" Class="w-100" />
</div>
}
<RadzenButton Text="Mint" Disabled="@string.IsNullOrEmpty(AppState.UserStorage.DestinationAddress)" Click="@(async () => await Mint())" />
}
</RadzenContent>

View File

@ -0,0 +1,35 @@
using CSharpFunctionalExtensions;
using NftFaucet.Components;
using NftFaucet.Utils;
using Radzen;
namespace NftFaucet.Pages;
public partial class MintPage : BasicComponent
{
private string SourceAddress { get; set; }
private bool IsReadyToMint => AppState != null &&
AppState.SelectedNetwork != null &&
AppState.SelectedProvider != null &&
AppState.SelectedProvider.IsConfigured &&
AppState.SelectedContract != null &&
AppState.SelectedToken != null &&
AppState.SelectedUploadLocation != null &&
AppState.UserStorage.DestinationAddress != null;
protected override async Task OnInitializedAsync()
{
if (AppState?.SelectedProvider?.IsConfigured ?? false)
{
SourceAddress = await ResultWrapper.Wrap(() => AppState.SelectedProvider.GetAddress()).Match(x => x, _ => null);
AppState.UserStorage.DestinationAddress = SourceAddress;
}
}
private async Task Mint()
{
await DialogService.OpenAsync<MintDialog>("Minting...",
new Dictionary<string, object>(),
new DialogOptions() { Width = "700px", Height = "570px", Resizable = true, Draggable = true });
}
}

View File

@ -0,0 +1,22 @@
@page "/networks"
@inherits BasicComponent
<PageTitle>Networks</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select network" />
<RadzenTabs RenderMode="TabRenderMode.Client">
<Tabs>
@if (Networks != null)
{
foreach (var kvp in Networks.OrderBy(x => x.Key))
{
var networkType = kvp.Key;
var networkCards = kvp.Value;
<RadzenTabsItem Text="@networkType.ToString()">
<CardList Data="@networkCards" OnSelectedChange="@(async _ => await OnNetworkChange())" @bind-SelectedItems="@AppState.UserStorage.SelectedNetworks"/>
</RadzenTabsItem>
}
}
</Tabs>
</RadzenTabs>
</RadzenContent>

View File

@ -0,0 +1,55 @@
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Plugins.NetworkPlugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class NetworksPage : BasicComponent
{
protected override void OnInitialized()
{
Networks = AppState.PluginStorage.Networks
.GroupBy(x => x.SubType)
.ToDictionary(x => x.Key, x => x.OrderBy(v => v.Order ?? int.MaxValue).Select(MapCardListItem).ToArray());
}
private Dictionary<NetworkSubtype, CardListItem[]> Networks { get; set; }
private CardListItem MapCardListItem(INetwork model)
=> new CardListItem
{
Id = model.Id,
ImageLocation = model.ImageName != null ? "./images/" + model.ImageName : null,
Header = model.Name,
IsDisabled = !model.IsSupported,
Properties = new[]
{
new CardListItemProperty { Name = "ChainID", Value = model.ChainId?.ToString() },
new CardListItemProperty { Name = "Currency", Value = model.Currency },
},
Badges = new[]
{
(Settings?.RecommendedNetworks?.Contains(model.Id) ?? false)
? new CardListItemBadge {Style = BadgeStyle.Success, Text = "Recommended"}
: null,
!model.IsSupported ? new CardListItemBadge { Style = BadgeStyle.Light, Text = "Not Supported" } : null,
!model.IsTestnet ? new CardListItemBadge { Style = BadgeStyle.Danger, Text = "Mainnet" } : null,
model.IsDeprecated ? new CardListItemBadge { Style = BadgeStyle.Warning, Text = "Deprecated" } : null,
}.Where(x => x != null).ToArray(),
};
private async Task OnNetworkChange()
{
var currentNetwork = AppState.SelectedNetwork;
AppState.UserStorage.SelectedContracts = Array.Empty<Guid>();
var currentProvider = AppState.SelectedProvider;
if (currentNetwork == null || (currentProvider != null && !currentProvider.IsNetworkSupported(currentNetwork)))
{
AppState.UserStorage.SelectedProviders = Array.Empty<Guid>();
}
await SaveAppState();
}
}

View File

@ -0,0 +1,15 @@
@page "/providers"
@inherits BasicComponent
<PageTitle>Providers</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select network provider" />
@if (AppState.SelectedNetwork == null)
{
<RadzenHeading Size="H3" Text="Please choose network first!" />
}
else
{
<CardList Data="@ProviderCards" OnSelectedChange="@(async _ => await OnProviderChange())" @bind-SelectedItems="@AppState.UserStorage.SelectedProviders" />
}
</RadzenContent>

View File

@ -0,0 +1,67 @@
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Plugins.ProviderPlugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class ProvidersPage : BasicComponent
{
protected override void OnInitialized()
{
Providers = AppState.PluginStorage.Providers.Where(x => AppState.SelectedNetwork != null && x.IsNetworkSupported(AppState.SelectedNetwork)).ToArray();
RefreshCards();
RefreshMediator.NotifyStateHasChangedSafe();
base.OnInitialized();
}
private IProvider[] Providers { get; set; }
private CardListItem[] ProviderCards { get; set; }
private void RefreshCards()
{
ProviderCards = Providers.Select(MapCardListItem).ToArray();
}
private CardListItem MapCardListItem(IProvider provider)
{
var configuration = provider.GetConfiguration();
return new CardListItem
{
Id = provider.Id,
ImageLocation = provider.ImageName != null ? "./images/" + provider.ImageName : null,
Header = provider.Name,
IsDisabled = !provider.IsSupported,
Properties = provider.GetProperties().ToArray(),
SelectionIcon = provider.IsConfigured ? CardListItemSelectionIcon.Checkmark : CardListItemSelectionIcon.Warning,
Badges = new[]
{
(Settings?.RecommendedProviders?.Contains(provider.Id) ?? false)
? new CardListItemBadge {Style = BadgeStyle.Success, Text = "Recommended"}
: null,
!provider.IsSupported
? new CardListItemBadge {Style = BadgeStyle.Light, Text = "Not Supported"}
: null,
}.Where(x => x != null).ToArray(),
Configuration = configuration == null ? null : new CardListItemConfiguration
{
Objects = configuration.Objects,
ConfigureAction = async x =>
{
var result = await configuration.ConfigureAction(x);
RefreshCards();
if (result.IsSuccess)
{
await StateRepository.SaveProviderState(provider);
}
return result;
},
},
};
}
private async Task OnProviderChange()
{
await SaveAppState();
}
}

View File

@ -1,34 +0,0 @@
@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>
<Button Type="@ButtonType.Primary" Style="width: 150px; height: 100px" Size="large" OnClick="OnEthereumSelected">
<div>Ethereum</div>
</Button>
<Button Type="@ButtonType.Primary" Style="width: 150px; height: 100px" Color="Color.Red5" Size="large" OnClick="OnSolanaSelected">
<div>Solana</div>
</Button>
</SpaceItem>
</Space>
<Space Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem>
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<Select DataSource="@ChainTypes"
DefaultValue="@(nameof(NetworkChain.SolanaDevnet))"
ValueName="@nameof(EnumWrapper<NetworkChain>.ValueString)"
LabelName="@nameof(EnumWrapper<NetworkChain>.Description)"
OnSelectedItemChanged="OnNetworkChange">
</Select>
}
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -1,35 +0,0 @@
using NftFaucet.Components;
using NftFaucet.Models.Enums;
namespace NftFaucet.Pages;
public class Step1Component : BasicComponent
{
protected EnumWrapper<NetworkType>[] NetworkTypes { get; } = Enum.GetValues<NetworkType>()
.Select(x => new EnumWrapper<NetworkType>(x, x.ToString())).ToArray();
protected EnumWrapper<NetworkChain>[] ChainTypes { get; } = new List<NetworkChain>() {NetworkChain.SolanaTestnet, NetworkChain.SolanaDevnet, NetworkChain.SolanaMainnet}
.Select(x => new EnumWrapper<NetworkChain>(x, x.ToString())).ToArray();
protected void OnEthereumSelected()
{
AppState.Storage.NetworkType = NetworkType.Ethereum;
AppState.Navigation.GoForward();
}
protected void OnSolanaSelected()
{
AppState.Storage.NetworkType = NetworkType.Solana;
AppState.Storage.NetworkChain = NetworkChain.SolanaDevnet;
}
protected void OnNetworkChange(EnumWrapper<NetworkChain> network)
{
AppState.Storage.NetworkChain = network.Value;
RefreshMediator.NotifyStateHasChangedSafe();
AppState.Navigation.GoForward();
}
}

View File

@ -1,177 +0,0 @@
@page "/step2"
@using NftFaucet.Models.Enums
@using Microsoft.AspNetCore.Components
@inherits Step2Component
<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>
<FileDropZone class="drop-zone">
<Upload Name="file" Class="@ImageClass" ListType="picture-card"
ShowUploadList="false" BeforeAllUploadAsync="BeforeUpload">
@if (AppState?.Storage?.UploadIsInProgress ?? false)
{
<div>
<Icon Spin="true" Type="loading" Style="font-size: 2rem;"></Icon>
<Title Level="4">Uploading...</Title>
</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" Style="font-size: 2rem;"></Icon>
<Title Level="4">Unable to preview</Title>
</div>
}
else
{
<div>
<Icon Type="plus" Style="font-size: 2rem;"></Icon>
<Title Level="4">Choose or drag & drop file</Title>
</div>
}
</Upload>
</FileDropZone>
</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>
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token symbol:</Title>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<div class="@SymbolClass">
<Input Size="medium" @bind-Value="@AppState.Storage.TokenSymbol" OnInput="@OnSymbolInputChange" />
</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.Crust))"
ValueName="@nameof(EnumWrapper<IpfsGatewayType>.ValueString)"
LabelName="@nameof(EnumWrapper<IpfsGatewayType>.Description)"
OnSelectedItemChanged="OnIpfsGatewayChange">
</Select>
</SpaceItem>
</Space>
</SpaceItem>
@if (AppState.Storage.NetworkType == NetworkType.Ethereum)
{
<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>
}
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Horizontal" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Include master edition:</Title>
</SpaceItem>
<SpaceItem>
<Popover Title="Warning" Content="Notice that for master edition token amount should always be 1">
<Checkbox Style="margin-top: auto; margin-bottom: auto" @bind-Value="AppState.Storage.IncludeMasterEdition" OnChange="OnMasterEditionCheck"/>
</Popover>
</SpaceItem>
</Space>
</SpaceItem>
}
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Horizontal" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Is token mutable:</Title>
</SpaceItem>
<SpaceItem>
<Checkbox Style="margin-top: auto; margin-bottom: auto" @bind-Value="AppState.Storage.IsTokenMutable"/>
</SpaceItem>
</Space>
</SpaceItem>
}
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Seller fee basis points:</Title>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.InputNumber @bind-Value="@AppState.Storage.SellerFeeBasisPoints" Min="1" Max="10000" DefaultValue="88"></AntDesign.InputNumber>
</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;">Amount:</Title>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.InputNumber @bind-Value="@AppState.Storage.TokenAmount" Disabled="@((AppState.Storage.NetworkType == NetworkType.Ethereum
&& AppState.Storage.TokenType == TokenType.ERC721) ||
(AppState.Storage.NetworkType == NetworkType.Solana &&
AppState.Storage.IncludeMasterEdition))" Min="1" Max="1000" DefaultValue="1"></AntDesign.InputNumber>
</div>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -1,141 +0,0 @@
using AntDesign;
using Microsoft.AspNetCore.Components;
using NftFaucet.Components;
using NftFaucet.Constants;
using NftFaucet.Extensions;
using NftFaucet.Models.Enums;
namespace NftFaucet.Pages;
public class Step2Component : BasicComponent
{
protected string NameErrorMessage { get; set; }
protected string DescriptionErrorMessage { get; set; }
protected string SymbolErrorMessage { 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 SymbolClass => string.IsNullOrWhiteSpace(SymbolErrorMessage) ? 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() || !AppState.IpfsContext.IsInitialized)
UriHelper.NavigateToRelative("/");
AppState.Navigation.SetForwardHandler(ForwardHandler);
}
protected Task<bool> ForwardHandler()
{
var isValidName = !string.IsNullOrWhiteSpace(AppState.Storage.TokenName);
var isValidDescription = !string.IsNullOrWhiteSpace(AppState.Storage.TokenDescription);
var isValidTokenSymbol = AppState.Storage.NetworkType != NetworkType.Solana || !string.IsNullOrEmpty(AppState.Storage.TokenSymbol);
var isValidFile = AppState.Storage.IpfsImageUrl != null;
var isNotUploading = !AppState.Storage.UploadIsInProgress;
if (!isValidName)
{
NameErrorMessage = "Invalid name";
}
if (!isValidDescription || !isValidTokenSymbol)
{
DescriptionErrorMessage = "Invalid description";
}
if (!isValidFile)
{
ImageErrorMessage = "Invalid file";
}
if (!isNotUploading)
{
ImageErrorMessage = "Upload is still in progress";
}
RefreshMediator.NotifyStateHasChangedSafe();
var canProceed = isValidName && isValidDescription && isValidTokenSymbol && isValidFile && isNotUploading;
return Task.FromResult(canProceed);
}
protected void OnNameInputChange(ChangeEventArgs args)
{
NameErrorMessage = string.Empty;
}
protected void OnDescriptionInputChange(ChangeEventArgs args)
{
DescriptionErrorMessage = string.Empty;
}
protected void OnSymbolInputChange(ChangeEventArgs args)
{
DescriptionErrorMessage = string.Empty;
}
protected void OnMasterEditionCheck(bool value)
{
if (value)
{
AppState.Storage.TokenAmount = 1;
}
}
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

@ -1,13 +0,0 @@
@page "/step3"
@inherits Step3Component
<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

@ -1,84 +0,0 @@
using System.Text;
using BlazorMonaco;
using Newtonsoft.Json;
using NftFaucet.Components;
using NftFaucet.Extensions;
using NftFaucet.Models.Token;
namespace NftFaucet.Pages;
public class Step3Component : 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.IpfsContext.IsInitialized || AppState.Storage.IpfsImageUrl == null)
UriHelper.NavigateToRelative("/");
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 = AppState?.Storage?.IpfsImageUrl != null
? IpfsService.GetUrlToGateway(AppState.Storage.IpfsImageUrl, AppState.Storage.IpfsGatewayType)
: null;
var metadata = new TokenMetadata
{
Name = AppState?.Storage?.TokenName,
Description = AppState?.Storage?.TokenDescription,
Image = imageUrl?.OriginalString,
ExternalUrl = "https://darkcodi.github.io/nft-faucet/",
};
var metadataJson = JsonConvert.SerializeObject(metadata, Formatting.Indented);
return metadataJson;
}
}

View File

@ -1,27 +0,0 @@
@page "/step4"
@inherits Step4Component
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
@if (!IsSupportedNetwork)
{
<SpaceItem>
<Title Level="2" Strong="true" Style="color: red;">Selected network is not supported. Please change network in Metamask!</Title>
</SpaceItem>
}
<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

@ -1,59 +0,0 @@
using NftFaucet.Components;
using NftFaucet.Extensions;
using NftFaucet.Models;
using NftFaucet.Models.Enums;
using Solnet.Wallet;
namespace NftFaucet.Pages;
public class Step4Component : 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 bool IsSupportedNetwork => AppState?.Metamask?.Network != null && Settings?.GetEthereumNetworkOptions(AppState.Metamask.Network!.Value) != null;
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || string.IsNullOrEmpty(AppState.Storage.TokenUrl))
UriHelper.NavigateToRelative("/");
AppState.Navigation.SetForwardHandler(ForwardHandler);
AppState.Storage.DestinationAddress = AppState.Storage.NetworkType == NetworkType.Solana
? "51CNhAWJ94HrvXLNJrbXzhzgSixpwvwYvXTA9U6itENE"
: 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 = AppState.Storage.NetworkType == NetworkType.Ethereum
? Address.Create(AppState.Storage.DestinationAddress).IsSuccess
: SolanaAddress.Create(AppState.Storage.DestinationAddress).IsSuccess;
if (!isValidTokenUri)
{
TokenUrlErrorMessage = "Invalid token URI";
}
if (!isValidDestinationAddress)
{
DestinationAddressErrorMessage = "Invalid destination address";
}
RefreshMediator.NotifyStateHasChangedSafe();
return Task.FromResult(isValidTokenUri && isValidDestinationAddress && IsSupportedNetwork);
}
}

View File

@ -1,49 +0,0 @@
@page "/step5"
@using NftFaucet.Models.Enums
@inherits Step5Component
@if (TransactionHash == null)
{
<Space Align="center" Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem Class="drk-full-height">
<Space Align="center" Direction="DirectionVHType.Vertical" Class="drk-vertical-space-center" Style="align-items: center !important;">
<SpaceItem>
<Spin size="large" />
</SpaceItem>
@if (AppState.Storage.NetworkType == NetworkType.Ethereum)
{
<SpaceItem>
<Title Level="3">MetaMask will show a transaction signing pop-up. Please approve.</Title>
</SpaceItem>
}
@if (AppState.Storage.NetworkType == NetworkType.Solana)
{
<SpaceItem>
<Title Level="3">Solana mint in progress. Please wait...</Title>
</SpaceItem>
}
</Space>
</SpaceItem>
</Space>
}
else if (TransactionHash.Value.IsSuccess)
{
<Result Status="success"
Title="Transaction was successfully created!"
SubTitle="@($"Transaction: {TransactionHash.Value.Value}. Please wait for 1-5 minutes till transaction is completed.")">
<Extra>
<Button OnClick="@ResetState">Start a new mint</Button>
<Button Type="@ButtonType.Primary" OnClick="@ViewOnExplorer">View on explorer</Button>
</Extra>
</Result>
}
else
{
<Result Status="error"
Title="Failed to create transaction"
SubTitle="@("Please ensure that you approve created transactions in Metamask")">
<Extra>
<Button Type="primary" OnClick="@RetryTransaction">Try again</Button>
</Extra>
</Result>
}

View File

@ -1,139 +0,0 @@
using CSharpFunctionalExtensions;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using NftFaucet.Components;
using NftFaucet.Extensions;
using NftFaucet.Models.Enums;
using NftFaucet.Services;
using NftFaucet.Utils;
namespace NftFaucet.Pages;
public class Step5Component : BasicComponent
{
[Inject]
public IIpfsService IpfsService { get; set; }
[Inject]
public IEthereumTransactionService TransactionService { get; set; }
[Inject]
public ISolanaTransactionService SolanaTransactionService { get; set; }
[Inject]
protected IJSRuntime JsRuntime { get; set; }
protected Result<string>? TransactionHash { get; set; }
protected override async Task OnInitializedAsync()
{
if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || string.IsNullOrEmpty(AppState.Storage.DestinationAddress))
{
UriHelper.NavigateToRelative("/");
}
else
{
Task.Run(Mint);
}
}
public async Task Mint()
{
var network = AppState.Metamask.Network!.Value;
var address = AppState.Storage.DestinationAddress;
var uri = AppState.Storage.TokenUrl;
if (AppState.Storage.NetworkType == NetworkType.Ethereum && AppState.Storage.TokenType == TokenType.ERC721)
{
TransactionHash = await ResultWrapper.Wrap(TransactionService.MintErc721Token(network, address, uri));
}
else if (AppState.Storage.NetworkType == NetworkType.Ethereum && AppState.Storage.TokenType == TokenType.ERC1155)
{
var amount = (int) AppState.Storage.TokenAmount;
TransactionHash = await ResultWrapper.Wrap(TransactionService.MintErc1155Token(network, address, amount, uri));
}
else if (AppState.Storage.NetworkType == NetworkType.Solana)
{
TransactionHash =
await ResultWrapper.Wrap(SolanaTransactionService.MintNft(AppState.Storage.NetworkChain,
address,
uri,
AppState.Storage.TokenName,
AppState.Storage.TokenSymbol,
AppState.Storage.IsTokenMutable,
AppState.Storage.IncludeMasterEdition,
(uint)AppState.Storage.SellerFeeBasisPoints,
(ulong)AppState.Storage.TokenAmount));
}
RefreshMediator.NotifyStateHasChangedSafe();
}
protected void ResetState()
{
AppState.Reset();
UriHelper.NavigateToRelative("/");
}
protected async Task ViewOnExplorer()
{
var network = AppState.Metamask.Network;
if (AppState.Storage.NetworkType == NetworkType.Solana)
{
network = AppState.Storage.NetworkChain;
}
var baseUrl = network switch
{
NetworkChain.EthereumMainnet => "https://etherscan.io/tx/",
NetworkChain.Ropsten => "https://ropsten.etherscan.io/tx/",
NetworkChain.Rinkeby => "https://rinkeby.etherscan.io/tx/",
NetworkChain.Goerli => "https://goerli.etherscan.io/tx/",
NetworkChain.Kovan => "https://kovan.etherscan.io/tx/",
NetworkChain.OptimismMainnet => "https://optimistic.etherscan.io/tx/",
NetworkChain.OptimismKovan => "https://kovan-optimistic.etherscan.io/tx/",
NetworkChain.PolygonMainnet => "https://polygonscan.com/tx/",
NetworkChain.PolygonMumbai => "https://mumbai.polygonscan.com/tx/",
NetworkChain.MoonbeamMainnet => "https://blockscout.moonbeam.network/tx/",
NetworkChain.MoonbaseAlpha => "https://moonbase.moonscan.io/tx/",
NetworkChain.ArbitrumMainnetBeta => "https://explorer.arbitrum.io/tx/",
NetworkChain.ArbitrumRinkeby => "https://testnet.arbiscan.io/tx/",
NetworkChain.ArbitrumGoerli => "https://nitro-devnet-explorer.arbitrum.io/tx/",
NetworkChain.AvalancheMainnet => "https://snowtrace.io/tx/",
NetworkChain.AvalancheFuji => "https://testnet.snowtrace.io/tx/",
NetworkChain.SolanaDevnet => "https://explorer.solana.com/tx/",
NetworkChain.SolanaTestnet => "https://explorer.solana.com/tx/",
NetworkChain.SolanaMainnet => "https://explorer.solana.com/tx/",
NetworkChain.BnbChainMainnet => "https://bscscan.com/tx/",
NetworkChain.BnbChainTestnet => "https://testnet.bscscan.com/tx/",
_ => null,
};
if (baseUrl == null && !network.HasValue)
return;
var txHash = TransactionHash!.Value!.Value;
var txUrl = BuildTxUrl(network.Value, baseUrl, txHash);
await JsRuntime.InvokeAsync<object>("open", txUrl, "_blank");
}
protected async Task RetryTransaction()
{
TransactionHash = null;
RefreshMediator.NotifyStateHasChangedSafe();
Mint();
}
private string BuildTxUrl(NetworkChain chain, string baseUrl, string txHash)
{
return chain switch
{
NetworkChain.SolanaDevnet => baseUrl + txHash + "?cluster=devnet",
NetworkChain.SolanaTestnet => baseUrl + txHash + "?cluster=testnet",
NetworkChain.SolanaMainnet => baseUrl + txHash,
_ => baseUrl + txHash,
};
}
}

View File

@ -0,0 +1,20 @@
@page "/tokens"
@inherits BasicComponent
<PageTitle>Tokens</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select or create a token to mint" />
<div style="width: 100%; display: flex; flex-direction: row; justify-content: end;">
<RadzenSplitButton Text="Import from..." Icon="backup" Style="margin-right: 1rem;" Click="@(() => NotificationService.Notify(NotificationSeverity.Warning, "NOT IMPLEMENTED", "Will be implemented later"))" >
<ChildContent>
<RadzenSplitButtonItem Text="OpenSea" Value="1" />
<RadzenSplitButtonItem Text="Rarible" Value="2" />
<RadzenSplitButtonItem Text="Nifty" Value="3" />
<RadzenSplitButtonItem Text="SuperRare" Value="4" />
</ChildContent>
</RadzenSplitButton>
<RadzenButton Text="Create New" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary"
Click="@OpenCreateTokenDialog"/>
</div>
<CardList Data="@TokenCards" OnSelectedChange="@(async _ => await OnTokenChange())" @bind-SelectedItems="@AppState.UserStorage.SelectedTokens" />
</RadzenContent>

View File

@ -0,0 +1,70 @@
using ByteSizeLib;
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Plugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class TokensPage : BasicComponent
{
protected override void OnInitialized()
{
RefreshCards();
}
private CardListItem[] TokenCards { get; set; }
private void RefreshCards()
{
TokenCards = AppState?.UserStorage?.Tokens?.Select(MapCardListItem).ToArray() ?? Array.Empty<CardListItem>();
}
private CardListItem MapCardListItem(IToken token)
=> new CardListItem
{
Id = token.Id,
Header = token.Name,
ImageLocation = token.Image.FileData,
Properties = new[]
{
new CardListItemProperty
{
Name = "Description",
Value = token.Description,
},
new CardListItemProperty
{
Name = "Size",
Value = ByteSize.FromBytes(token.Image.FileSize).ToString(),
},
},
};
private async Task OpenCreateTokenDialog()
{
var token = (IToken) await DialogService.OpenAsync<CreateTokenDialog>("Create new token",
new Dictionary<string, object>(),
new DialogOptions() { Width = "700px", Height = "570px", Resizable = true, Draggable = true });
if (token == null)
{
return;
}
AppState.UserStorage.Tokens ??= new List<IToken>();
AppState.UserStorage.Tokens.Add(token);
AppState.UserStorage.SelectedTokens = new[] { token.Id };
AppState.UserStorage.SelectedUploadLocations = Array.Empty<Guid>();
RefreshCards();
RefreshMediator.NotifyStateHasChangedSafe();
await StateRepository.SaveToken(token);
await SaveAppState();
}
private async Task OnTokenChange()
{
AppState.UserStorage.SelectedUploadLocations = Array.Empty<Guid>();
await SaveAppState();
}
}

View File

@ -0,0 +1,19 @@
@page "/uploads"
@inherits BasicComponent
<PageTitle>Token upload locations</PageTitle>
<RadzenContent Container="main">
<RadzenHeading Size="H1" Text="Select or create a token upload" />
@if (AppState.SelectedToken == null)
{
<RadzenHeading Size="H3" Text="Please choose token first!" />
}
else
{
<div style="width: 100%; display: flex; flex-direction: row; justify-content: end;">
<RadzenButton Text="Create New" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary"
Click="@OpenCreateUploadDialog"/>
</div>
<CardList Data="@UploadCards" OnSelectedChange="@(async _ => await OnUploadLocationChange())" @bind-SelectedItems="@AppState.UserStorage.SelectedUploadLocations"/>
}
</RadzenContent>

View File

@ -0,0 +1,89 @@
using System.Globalization;
using NftFaucet.Components;
using NftFaucet.Components.CardList;
using NftFaucet.Plugins;
using Radzen;
namespace NftFaucet.Pages;
public partial class UploadLocationsPage : BasicComponent
{
protected override void OnInitialized()
{
RefreshCards();
}
private CardListItem[] UploadCards { get; set; }
private void RefreshCards()
{
var selectedTokenId = AppState?.UserStorage?.SelectedTokens?.FirstOrDefault();
UploadCards = AppState?.UserStorage?.UploadLocations?.Where(x => x.TokenId == selectedTokenId).Select(MapCardListItem).ToArray() ?? Array.Empty<CardListItem>();
}
private CardListItem MapCardListItem(ITokenUploadLocation uploadLocation)
=> new CardListItem
{
Id = uploadLocation.Id,
Header = uploadLocation.Name,
ImageLocation = GetUploaderImageLocation(uploadLocation.UploaderId),
Properties = new[]
{
new CardListItemProperty
{
Name = "Id",
Value = uploadLocation.Id.ToString(),
},
new CardListItemProperty
{
Name = "Location",
Value = uploadLocation.Location,
Link = uploadLocation.Location,
},
new CardListItemProperty
{
Name = "CreatedAt",
Value = uploadLocation.CreatedAt.ToString(CultureInfo.InvariantCulture),
},
},
};
private string GetUploaderImageLocation(Guid uploaderId)
{
var uploader = AppState?.PluginStorage?.Uploaders?.FirstOrDefault(x => x.Id == uploaderId);
if (uploader == null)
{
return null;
}
return "./images/" + uploader.ImageName;
}
private async Task OpenCreateUploadDialog()
{
var uploadLocation = (ITokenUploadLocation) await DialogService.OpenAsync<CreateUploadDialog>("Create new upload",
new Dictionary<string, object>
{
{ "Token", AppState.SelectedToken },
},
new DialogOptions() { Width = "1000px", Height = "700px", Resizable = true, Draggable = true });
if (uploadLocation == null)
{
return;
}
AppState.UserStorage.UploadLocations ??= new List<ITokenUploadLocation>();
AppState.UserStorage.UploadLocations.Add(uploadLocation);
AppState.UserStorage.SelectedUploadLocations = new[] { uploadLocation.Id };
RefreshCards();
RefreshMediator.NotifyStateHasChangedSafe();
await StateRepository.SaveUploadLocation(uploadLocation);
await SaveAppState();
}
private async Task OnUploadLocationChange()
{
await SaveAppState();
}
}

View File

@ -0,0 +1,10 @@
namespace NftFaucet.Plugins;
public interface IToken
{
public Guid Id { get; }
public string Name { get; }
public string Description { get; }
public DateTime CreatedAt { get; }
public ITokenMedia Image { get; }
}

View File

@ -0,0 +1,9 @@
namespace NftFaucet.Plugins;
public interface ITokenMedia
{
public string FileName { get; }
public string FileType { get; }
public string FileData { get; }
public long FileSize { get; }
}

View File

@ -0,0 +1,11 @@
namespace NftFaucet.Plugins;
public interface ITokenUploadLocation
{
public Guid Id { get; }
public Guid TokenId { get; }
public string Name { get; }
public string Location { get; }
public DateTime CreatedAt { get; }
public Guid UploaderId { get; }
}

View File

@ -0,0 +1,13 @@
using NftFaucet.Plugins.NetworkPlugins.Arbitrum.Networks;
namespace NftFaucet.Plugins.NetworkPlugins.Arbitrum;
public class ArbitrumNetworkPlugin : INetworkPlugin
{
public IReadOnlyCollection<INetwork> Networks { get; } = new INetwork[]
{
new ArbitrumOneNetwork(),
new ArbitrumNovaNetwork(),
new ArbitrumRinkebyNetwork(),
};
}

View File

@ -0,0 +1,20 @@
namespace NftFaucet.Plugins.NetworkPlugins.Arbitrum.Networks;
public class ArbitrumNovaNetwork : INetwork
{
public Guid Id { get; } = Guid.Parse("e2f056f8-1c5c-494f-9e88-96213a2009d4");
public string Name { get; } = "Arbitrum Nova";
public string ShortName { get; } = "ArbNova";
public ulong? ChainId { get; } = 42170;
public int? Order { get; } = 2;
public string Currency { get; } = "ETH";
public string ImageName { get; } = "arbitrum-black.svg";
public bool IsSupported { get; } = false;
public bool IsTestnet { get; } = true;
public bool IsDeprecated { get; } = false;
public NetworkType Type { get; } = NetworkType.Ethereum;
public NetworkSubtype SubType { get; } = NetworkSubtype.Arbitrum;
public Uri PublicRpcUrl { get; } = null;
public Uri ExplorerUrl { get; } = new Uri("https://nova-explorer.arbitrum.io");
public IReadOnlyCollection<IContract> DeployedContracts { get; } = Array.Empty<IContract>();
}

View File

@ -0,0 +1,20 @@
namespace NftFaucet.Plugins.NetworkPlugins.Arbitrum.Networks;
public class ArbitrumOneNetwork : INetwork
{
public Guid Id { get; } = Guid.Parse("4f0be8b9-dda1-4598-88b9-d4ba77f4c30e");
public string Name { get; } = "Arbitrum One";
public string ShortName { get; } = "Arbitrum";
public ulong? ChainId { get; } = 42161;
public int? Order { get; } = 1;
public string Currency { get; } = "ETH";
public string ImageName { get; } = "arbitrum.svg";
public bool IsSupported { get; } = false;
public bool IsTestnet { get; } = false;
public bool IsDeprecated { get; } = false;
public NetworkType Type { get; } = NetworkType.Ethereum;
public NetworkSubtype SubType { get; } = NetworkSubtype.Arbitrum;
public Uri PublicRpcUrl { get; } = null;
public Uri ExplorerUrl { get; } = new Uri("https://arbiscan.io/");
public IReadOnlyCollection<IContract> DeployedContracts { get; } = Array.Empty<IContract>();
}

View File

@ -0,0 +1,46 @@
using System.Globalization;
namespace NftFaucet.Plugins.NetworkPlugins.Arbitrum.Networks;
public class ArbitrumRinkebyNetwork : INetwork
{
public Guid Id { get; } = Guid.Parse("8189f9cd-14fc-41ab-9418-ca472ab15873");
public string Name { get; } = "Arbitrum Rinkeby";
public string ShortName { get; } = "ArbRinkeby";
public ulong? ChainId { get; } = 421611;
public int? Order { get; } = 3;
public string Currency { get; } = "ETH";
public string ImageName { get; } = "arbitrum-black.svg";
public bool IsSupported { get; } = true;
public bool IsTestnet { get; } = true;
public bool IsDeprecated { get; } = false;
public NetworkType Type { get; } = NetworkType.Ethereum;
public NetworkSubtype SubType { get; } = NetworkSubtype.Arbitrum;
public Uri PublicRpcUrl { get; } = new Uri("https://rinkeby.arbitrum.io/rpc");
public Uri ExplorerUrl { get; } = new Uri("https://rinkeby-explorer.arbitrum.io/");
public IReadOnlyCollection<IContract> DeployedContracts { get; } = new[]
{
new Contract
{
Id = Guid.Parse("f82fc396-1cfd-4db7-af27-9e3e7d4264f6"),
Name = "ERC-721 Faucet",
Symbol = "FA721",
Address = "0x9F64932Be34D5D897C4253D17707b50921f372B6",
Type = ContractType.Erc721,
DeploymentTxHash = "0x506e9c08cb360eb85b0b5d1e86615178ff1e4c0fab05297e725e9d227f45fe6b",
DeployedAt = DateTime.Parse("Apr-17-2022 01:45:55 PM", CultureInfo.InvariantCulture),
IsVerified = true,
},
new Contract
{
Id = Guid.Parse("6e125364-b8fd-49fe-b453-cfa69ebad811"),
Name = "ERC-1155 Faucet",
Symbol = "FA1155",
Address = "0xf67C575502fc1cE399a3e1895dDf41847185D7bD",
Type = ContractType.Erc1155,
DeploymentTxHash = "0xe62c31c70fd37a4b984b4b51d2ee5e08cd6bc53983200ca0041656f57f6dc8de",
DeployedAt = DateTime.Parse("Apr-17-2022 01:49:41 PM", CultureInfo.InvariantCulture),
IsVerified = true,
},
};
}

Some files were not shown because too many files have changed in this diff Show More