Update pages

This commit is contained in:
Yurii Didyk 2022-06-03 17:33:28 +03:00
parent 1e9d5b0243
commit 84214e8999
19 changed files with 644 additions and 485 deletions

View File

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

View File

@ -4,6 +4,4 @@ public enum TokenType : byte
{ {
ERC721 = 0, ERC721 = 0,
ERC1155 = 1, ERC1155 = 1,
SolanaDevnet = 2,
SolanaTestnet = 3
} }

View File

@ -1,4 +1,5 @@
using CSharpFunctionalExtensions; using System.Text.RegularExpressions;
using CSharpFunctionalExtensions;
using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Util; using Nethereum.Util;
@ -16,9 +17,16 @@ public class SolanaAddress : ValueObject<SolanaAddress>
public static implicit operator string(SolanaAddress address) => address.Value; public static implicit operator string(SolanaAddress address) => address.Value;
public static explicit operator SolanaAddress(string address) => Create(address).Value; public static explicit operator SolanaAddress(string address) => Create(address).Value;
public static Result<SolanaAddress> Create(string address) public static Result<SolanaAddress> Create(string value)
{ {
return new SolanaAddress(address); var regex = "[1-9A-HJ-NP-Za-km-z]";
if (!Regex.IsMatch(value, regex))
{
return Result.Failure<SolanaAddress>("Invalid base58 string");
}
return new SolanaAddress(value);
} }
public override string ToString() => Value; public override string ToString() => Value;

View File

@ -16,4 +16,9 @@ public class StateStorage
public string TokenMetadata { get; set; } public string TokenMetadata { get; set; }
public string TokenUrl { get; set; } public string TokenUrl { get; set; }
public string DestinationAddress { get; set; } public string DestinationAddress { get; set; }
public NetworkType NetworkType { get; set; }
public EthereumNetwork Network { get; set; }
public string TokenSymbol { get; set; } = "DFNT";
public bool IsTokenMutable { get; set; } = true;
public double SellerFeeBasisPoints { get; set; } = 88;
} }

View File

@ -1,4 +1,4 @@
@page "/step1" @page "/step1"
@using NftFaucet.Models.Enums @using NftFaucet.Models.Enums
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@inherits Step1Component @inherits Step1Component
@ -7,107 +7,28 @@
<SpaceItem Class="drk-full-width"> <SpaceItem Class="drk-full-width">
<Space Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center"> <Space Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem> <SpaceItem>
<FileDropZone class="drop-zone"> <Button Type="@ButtonType.Primary" Size="large" OnClick="OnEthereumSelected">
<Upload Name="file" Class="@ImageClass" ListType="picture-card" <div>Ethereum</div>
ShowUploadList="false" BeforeAllUploadAsync="BeforeUpload"> </Button>
@if (AppState?.Storage?.UploadIsInProgress ?? false) <Button Type="@ButtonType.Primary" Size="large" OnClick="OnSolanaSelected">
{ <div>Solana</div>
<div> </Button>
<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> </SpaceItem>
</Space> </Space>
</SpaceItem> <Space Direction="DirectionVHType.Horizontal" Class="drk-vertical-space-center">
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem> <SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Description:</Title> @if (AppState.Storage.NetworkType == NetworkType.Solana)
</SpaceItem> {
<SpaceItem Class="drk-full-width"> <Select DataSource="@ChainTypes"
<div class="@DescriptionClass"> DefaultValue="@(nameof(EthereumNetwork.SolanaDevnet))"
<TextArea ShowCount MaxLength="255" MinRows="3" MaxRows="5" OnInput="@OnDescriptionInputChange" @bind-Value="@AppState.Storage.TokenDescription" /> ValueName="@nameof(EnumWrapper<EthereumNetwork>.ValueString)"
</div> LabelName="@nameof(EnumWrapper<EthereumNetwork>.Description)"
</SpaceItem> OnSelectedItemChanged="OnNetworkChange">
</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.Infura))"
ValueName="@nameof(EnumWrapper<IpfsGatewayType>.ValueString)"
LabelName="@nameof(EnumWrapper<IpfsGatewayType>.Description)"
OnSelectedItemChanged="OnIpfsGatewayChange">
</Select> </Select>
</SpaceItem> }
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token type:</Title>
</SpaceItem>
<SpaceItem>
<Select DataSource="@TokenTypes"
DefaultValue="@(nameof(TokenType.ERC721))"
ValueName="@nameof(EnumWrapper<TokenType>.ValueString)"
LabelName="@nameof(EnumWrapper<TokenType>.Description)"
OnSelectedItemChanged="OnTokenTypeChange">
</Select>
</SpaceItem>
</Space>
</SpaceItem>
<SpaceItem Class="drk-full-width">
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Amount:</Title>
</SpaceItem>
<SpaceItem>
<div>
<AntDesign.InputNumber @bind-Value="@AppState.Storage.TokenAmount" Disabled="@(AppState.Storage.TokenType == TokenType.ERC721)" Min="1" Max="1000" DefaultValue="1"></AntDesign.InputNumber>
</div>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem> </SpaceItem>
</Space> </Space>
</SpaceItem> </SpaceItem>
</Space> </Space>

View File

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

View File

@ -1,13 +1,162 @@
@page "/step2" @page "/step2"
@using NftFaucet.Models.Enums
@using Microsoft.AspNetCore.Components
@inherits Step2Component @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"> <Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
<SpaceItem> <SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token metadata:</Title> <Title Level="4" Style="margin-bottom: 0;">Name:</Title>
</SpaceItem> </SpaceItem>
<SpaceItem Class="drk-full-width"> <SpaceItem Class="drk-full-width">
<div class="@EditorClass"> <div class="@NameClass">
<MonacoEditor @ref="Editor" Id="metadata-monaco-editor" ConstructionOptions="EditorConstructionOptions"/> <Input Size="medium" @bind-Value="@AppState.Storage.TokenName" OnInput="@OnNameInputChange" />
</div> </div>
</SpaceItem> </SpaceItem>
</Space> </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;">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;">Description:</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.Infura))"
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.Vertical" Class="drk-full-width">
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Is token mutable:</Title>
</SpaceItem>
<SpaceItem>
<Checkbox @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.TokenType == TokenType.ERC721)" Min="1" Max="1000" DefaultValue="1"></AntDesign.InputNumber>
</div>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>
</SpaceItem>
</Space>

View File

@ -1,84 +1,135 @@
using System.Text; using AntDesign;
using BlazorMonaco; using Microsoft.AspNetCore.Components;
using Newtonsoft.Json;
using NftFaucet.Components; using NftFaucet.Components;
using NftFaucet.Constants;
using NftFaucet.Extensions; using NftFaucet.Extensions;
using NftFaucet.Models.Token; using NftFaucet.Models.Enums;
namespace NftFaucet.Pages; namespace NftFaucet.Pages;
public class Step2Component : BasicComponent public class Step2Component : BasicComponent
{ {
protected MonacoEditor Editor { get; set; } protected string NameErrorMessage { get; set; }
protected string EditorErrorMessage { get; set; } protected string DescriptionErrorMessage { get; set; }
protected string EditorClass => string.IsNullOrWhiteSpace(EditorErrorMessage) ? null : "invalid-input"; 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 StandaloneEditorConstructionOptions EditorConstructionOptions(MonacoEditor editor) protected EnumWrapper<IpfsGatewayType>[] IpfsGateways { get; } = Enum.GetValues<IpfsGatewayType>()
{ .Select(x => new EnumWrapper<IpfsGatewayType>(x, x.ToString())).ToArray();
return new StandaloneEditorConstructionOptions
{ protected EnumWrapper<TokenType>[] TokenTypes { get; } = Enum.GetValues<TokenType>()
Language = "json", .Select(x => new EnumWrapper<TokenType>(x, x.ToString())).ToArray();
GlyphMargin = true,
Value = GetCurrentMetadataJson(),
};
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || AppState.Storage.IpfsImageUrl == null) if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized)
UriHelper.NavigateToRelative("/"); UriHelper.NavigateToRelative("/");
AppState.Navigation.SetForwardHandler(ForwardHandler); AppState.Navigation.SetForwardHandler(ForwardHandler);
await Task.Yield();
await Editor.SetValue(GetCurrentMetadataJson());
} }
protected async Task<bool> ForwardHandler() protected Task<bool> ForwardHandler()
{ {
var metadataJson = await Editor.GetValue(); var isValidName = !string.IsNullOrWhiteSpace(AppState.Storage.TokenName);
if (!metadataJson.IsValidJson()) var isValidDescription = AppState.Storage.NetworkType == NetworkType.Ethereum
? !string.IsNullOrWhiteSpace(AppState.Storage.TokenDescription)
: !string.IsNullOrEmpty(AppState.Storage.TokenSymbol);
var isValidFile = AppState.Storage.IpfsImageUrl != null;
var isNotUploading = !AppState.Storage.UploadIsInProgress;
if (!isValidName)
{ {
EditorErrorMessage = "Invalid JSON"; NameErrorMessage = "Invalid name";
}
if (!isValidDescription)
{
DescriptionErrorMessage = "Invalid description";
}
if (!isValidFile)
{
ImageErrorMessage = "Invalid file";
}
if (!isNotUploading)
{
ImageErrorMessage = "Upload is still in progress";
}
RefreshMediator.NotifyStateHasChangedSafe(); RefreshMediator.NotifyStateHasChangedSafe();
var canProceed = isValidName && isValidDescription && isValidFile && isNotUploading;
return Task.FromResult(canProceed);
}
protected void OnNameInputChange(ChangeEventArgs args)
{
NameErrorMessage = string.Empty;
}
protected void OnDescriptionInputChange(ChangeEventArgs args)
{
DescriptionErrorMessage = string.Empty;
}
protected void OnSymbolInputChange(ChangeEventArgs args)
{
DescriptionErrorMessage = string.Empty;
}
protected void OnIpfsGatewayChange(EnumWrapper<IpfsGatewayType> ipfsGatewayItem)
{
AppState.Storage.IpfsGatewayType = ipfsGatewayItem.Value;
}
protected void OnTokenTypeChange(EnumWrapper<TokenType> tokenTypeItem)
{
AppState.Storage.TokenType = tokenTypeItem.Value;
if (AppState.Storage.TokenType == TokenType.ERC721)
{
AppState.Storage.TokenAmount = 1;
}
RefreshMediator.NotifyStateHasChangedSafe();
}
protected async Task<bool> BeforeUpload(List<UploadFileItem> files)
{
var file = files.FirstOrDefault();
if (file == null)
{
return false; return false;
} }
if (AppState.Storage.TokenMetadata == metadataJson) var hasValidSize = file.Size < UploadConstants.MaxFileSizeInBytes;
if (!hasValidSize)
{ {
return true; MessageService.Error($"File must be smaller than {UploadConstants.MaxFileSizeInMegabytes} MB!");
return false;
} }
ImageErrorMessage = string.Empty;
AppState.Storage.UploadIsInProgress = true; AppState.Storage.UploadIsInProgress = true;
RefreshMediator.NotifyStateHasChangedSafe(); RefreshMediator.NotifyStateHasChangedSafe();
var metadataBytes = Encoding.UTF8.GetBytes(metadataJson); AppState.Storage.IpfsImageUrl = await IpfsService.Upload(file.FileName, file.Type, file.ObjectURL);
var tokenUri = await IpfsService.Upload("token.json", "application/json", metadataBytes); AppState.Storage.LocalImageUrl = new Uri(file.ObjectURL);
tokenUri = IpfsService.GetUrlToGateway(tokenUri, AppState.Storage.IpfsGatewayType); ImageErrorMessage = string.Empty;
AppState.Storage.TokenMetadata = metadataJson;
AppState.Storage.TokenUrl = tokenUri.OriginalString;
AppState.Storage.UploadIsInProgress = false; 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(); RefreshMediator.NotifyStateHasChangedSafe();
return true; return false;
}
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

@ -2,26 +2,12 @@
@inherits Step3Component @inherits Step3Component
<Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width"> <Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
@if (!IsSupportedNetwork)
{
<SpaceItem> <SpaceItem>
<Title Level="2" Strong="true" Style="color: red;">Selected network is not supported. Please change network in Metamask!</Title> <Title Level="4" Style="margin-bottom: 0;">Token metadata:</Title>
</SpaceItem>
}
<SpaceItem>
<Title Level="4" Style="margin-bottom: 0;">Token URI:</Title>
</SpaceItem> </SpaceItem>
<SpaceItem Class="drk-full-width"> <SpaceItem Class="drk-full-width">
<div class="@TokenUrlClass"> <div class="@EditorClass">
<Input Size="medium" @bind-Value="@AppState.Storage.TokenUrl" OnInput="@OnTokenUrlInputChange"/> <MonacoEditor @ref="Editor" Id="metadata-monaco-editor" ConstructionOptions="EditorConstructionOptions"/>
</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> </div>
</SpaceItem> </SpaceItem>
</Space> </Space>

View File

@ -1,53 +1,84 @@
using System.Text;
using BlazorMonaco;
using Newtonsoft.Json;
using NftFaucet.Components; using NftFaucet.Components;
using NftFaucet.Extensions; using NftFaucet.Extensions;
using NftFaucet.Models; using NftFaucet.Models.Token;
namespace NftFaucet.Pages; namespace NftFaucet.Pages;
public class Step3Component : BasicComponent public class Step3Component : BasicComponent
{ {
protected string TokenUrlErrorMessage { get; set; } protected MonacoEditor Editor { get; set; }
protected string DestinationAddressErrorMessage { get; set; } protected string EditorErrorMessage { get; set; }
protected string TokenUrlClass => string.IsNullOrWhiteSpace(TokenUrlErrorMessage) ? null : "invalid-input"; protected string EditorClass => string.IsNullOrWhiteSpace(EditorErrorMessage) ? 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 StandaloneEditorConstructionOptions EditorConstructionOptions(MonacoEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "json",
GlyphMargin = true,
Value = GetCurrentMetadataJson(),
};
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || string.IsNullOrEmpty(AppState.Storage.TokenUrl)) if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || AppState.Storage.IpfsImageUrl == null)
UriHelper.NavigateToRelative("/"); UriHelper.NavigateToRelative("/");
AppState.Navigation.SetForwardHandler(ForwardHandler); AppState.Navigation.SetForwardHandler(ForwardHandler);
AppState.Storage.DestinationAddress = AppState.Metamask.Address;
await Task.Yield();
await Editor.SetValue(GetCurrentMetadataJson());
} }
protected void OnTokenUrlInputChange() protected async Task<bool> ForwardHandler()
{ {
TokenUrlErrorMessage = string.Empty; var metadataJson = await Editor.GetValue();
if (!metadataJson.IsValidJson())
{
EditorErrorMessage = "Invalid JSON";
RefreshMediator.NotifyStateHasChangedSafe();
return false;
} }
protected void OnDestinationAddressInputChange() if (AppState.Storage.TokenMetadata == metadataJson)
{ {
DestinationAddressErrorMessage = string.Empty; return true;
}
protected Task<bool> ForwardHandler()
{
var isValidTokenUri = !string.IsNullOrWhiteSpace(AppState.Storage.TokenUrl);
var isValidDestinationAddress = Address.Create(AppState.Storage.DestinationAddress).IsSuccess || SolanaAddress.Create(AppState.Storage.DestinationAddress).IsSuccess;
if (!isValidTokenUri)
{
TokenUrlErrorMessage = "Invalid token URI";
}
if (!isValidDestinationAddress)
{
DestinationAddressErrorMessage = "Invalid destination address";
} }
AppState.Storage.UploadIsInProgress = true;
RefreshMediator.NotifyStateHasChangedSafe(); RefreshMediator.NotifyStateHasChangedSafe();
return Task.FromResult(isValidTokenUri && isValidDestinationAddress && IsSupportedNetwork); 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,39 +1,27 @@
@page "/step4" @page "/step4"
@inherits Step4Component @inherits Step4Component
@if (TransactionHash == null) <Space Align="start" Direction="DirectionVHType.Vertical" Class="drk-full-width">
@if (!IsSupportedNetwork)
{ {
<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> <SpaceItem>
<Spin size="large" /> <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>
<SpaceItem> <SpaceItem>
<Title Level="3">MetaMask will show a transaction signing pop-up. Please approve.</Title> <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> </SpaceItem>
</Space> </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,146 +1,56 @@
using CSharpFunctionalExtensions;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using NftFaucet.Components; using NftFaucet.Components;
using NftFaucet.Extensions; using NftFaucet.Extensions;
using NftFaucet.Models;
using NftFaucet.Models.Enums; using NftFaucet.Models.Enums;
using NftFaucet.Services;
using NftFaucet.Utils;
namespace NftFaucet.Pages; namespace NftFaucet.Pages;
public class Step4Component : BasicComponent public class Step4Component : BasicComponent
{ {
[Inject] protected string TokenUrlErrorMessage { get; set; }
public IIpfsService IpfsService { get; set; } protected string DestinationAddressErrorMessage { get; set; }
protected string TokenUrlClass => string.IsNullOrWhiteSpace(TokenUrlErrorMessage) ? null : "invalid-input";
[Inject] protected string DestinationAddressClass => string.IsNullOrWhiteSpace(DestinationAddressErrorMessage) ? null : "invalid-input";
public IEthereumTransactionService TransactionService { get; set; } protected bool IsSupportedNetwork => AppState?.Metamask?.Network != null && Settings?.GetEthereumNetworkOptions(AppState.Metamask.Network!.Value) != null;
[Inject]
public ISolanaTransactionService SolanaTransactionService { get; set; }
[Inject]
protected IJSRuntime JsRuntime { get; set; }
protected Result<string>? TransactionHash { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || string.IsNullOrEmpty(AppState.Storage.DestinationAddress)) if (!await AppState.Metamask.IsReady() || !AppState.IpfsContext.IsInitialized || string.IsNullOrEmpty(AppState.Storage.TokenUrl))
{
UriHelper.NavigateToRelative("/"); UriHelper.NavigateToRelative("/");
}
else AppState.Navigation.SetForwardHandler(ForwardHandler);
{ AppState.Storage.DestinationAddress = AppState.Metamask.Address;
Task.Run(Mint);
}
} }
public async Task Mint() protected void OnTokenUrlInputChange()
{ {
var network = AppState.Metamask.Network!.Value; TokenUrlErrorMessage = string.Empty;
var address = AppState.Storage.DestinationAddress; }
var uri = AppState.Storage.TokenUrl;
if (AppState.Storage.TokenType == TokenType.ERC721) protected void OnDestinationAddressInputChange()
{ {
TransactionHash = await ResultWrapper.Wrap(TransactionService.MintErc721Token(network, address, uri)); DestinationAddressErrorMessage = string.Empty;
} }
else if (AppState.Storage.TokenType == TokenType.ERC1155)
protected Task<bool> ForwardHandler()
{ {
var amount = (int) AppState.Storage.TokenAmount; var isValidTokenUri = !string.IsNullOrWhiteSpace(AppState.Storage.TokenUrl);
TransactionHash = await ResultWrapper.Wrap(TransactionService.MintErc1155Token(network, address, amount, uri)); 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";
} }
else if (AppState.Storage.TokenType == TokenType.SolanaDevnet)
if (!isValidDestinationAddress)
{ {
TransactionHash = DestinationAddressErrorMessage = "Invalid destination address";
await ResultWrapper.Wrap(SolanaTransactionService.MintNft(EthereumNetwork.SolanaDevnet,
address,
uri,
AppState.Storage.TokenName,
AppState.Storage.TokenAmount));
}
else if (AppState.Storage.TokenType == TokenType.SolanaTestnet)
{
TransactionHash =
await ResultWrapper.Wrap(SolanaTransactionService.MintNft(EthereumNetwork.SolanaTestnet,
address,
uri,
AppState.Storage.TokenName,
AppState.Storage.TokenAmount));
} }
RefreshMediator.NotifyStateHasChangedSafe(); RefreshMediator.NotifyStateHasChangedSafe();
}
protected void ResetState() return Task.FromResult(isValidTokenUri && isValidDestinationAddress && IsSupportedNetwork);
{
AppState.Reset();
UriHelper.NavigateToRelative("/");
}
protected async Task ViewOnExplorer()
{
var network = AppState.Metamask.Network;
if (AppState.Storage.TokenType == TokenType.SolanaDevnet)
{
network = EthereumNetwork.SolanaDevnet;
}
if (AppState.Storage.TokenType == TokenType.SolanaTestnet)
{
network = EthereumNetwork.SolanaTestnet;
}
var baseUrl = network switch
{
EthereumNetwork.EthereumMainnet => "https://etherscan.io/tx/",
EthereumNetwork.Ropsten => "https://ropsten.etherscan.io/tx/",
EthereumNetwork.Rinkeby => "https://rinkeby.etherscan.io/tx/",
EthereumNetwork.Goerli => "https://goerli.etherscan.io/tx/",
EthereumNetwork.Kovan => "https://kovan.etherscan.io/tx/",
EthereumNetwork.OptimismMainnet => "https://optimistic.etherscan.io/tx/",
EthereumNetwork.OptimismKovan => "https://kovan-optimistic.etherscan.io/tx/",
EthereumNetwork.PolygonMainnet => "https://polygonscan.com/tx/",
EthereumNetwork.PolygonMumbai => "https://mumbai.polygonscan.com/tx/",
EthereumNetwork.MoonbeamMainnet => "https://blockscout.moonbeam.network/tx/",
EthereumNetwork.MoonbaseAlpha => "https://moonbase.moonscan.io/tx/",
EthereumNetwork.ArbitrumMainnetBeta => "https://explorer.arbitrum.io/tx/",
EthereumNetwork.ArbitrumRinkeby => "https://testnet.arbiscan.io/tx/",
EthereumNetwork.ArbitrumGoerli => "https://nitro-devnet-explorer.arbitrum.io/tx/",
EthereumNetwork.AvalancheMainnet => "https://snowtrace.io/tx/",
EthereumNetwork.AvalancheFuji => "https://testnet.snowtrace.io/tx/",
EthereumNetwork.SolanaDevnet => "https://explorer.solana.com/tx/",
EthereumNetwork.SolanaTestnet => "https://explorer.solana.com/tx/",
EthereumNetwork.SolanaMainnet => "https://explorer.solana.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(EthereumNetwork chain, string baseUrl, string txHash)
{
return chain switch
{
EthereumNetwork.SolanaDevnet => baseUrl + txHash + "?cluster=devnet",
EthereumNetwork.SolanaTestnet => baseUrl + txHash + "?cluster=testnet",
EthereumNetwork.SolanaMainnet => baseUrl + txHash,
_ => baseUrl + txHash,
};
} }
} }

View File

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

@ -0,0 +1,136 @@
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.Network,
address,
uri,
AppState.Storage.TokenName,
AppState.Storage.TokenSymbol,
AppState.Storage.IsTokenMutable,
(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.Network;
}
var baseUrl = network switch
{
EthereumNetwork.EthereumMainnet => "https://etherscan.io/tx/",
EthereumNetwork.Ropsten => "https://ropsten.etherscan.io/tx/",
EthereumNetwork.Rinkeby => "https://rinkeby.etherscan.io/tx/",
EthereumNetwork.Goerli => "https://goerli.etherscan.io/tx/",
EthereumNetwork.Kovan => "https://kovan.etherscan.io/tx/",
EthereumNetwork.OptimismMainnet => "https://optimistic.etherscan.io/tx/",
EthereumNetwork.OptimismKovan => "https://kovan-optimistic.etherscan.io/tx/",
EthereumNetwork.PolygonMainnet => "https://polygonscan.com/tx/",
EthereumNetwork.PolygonMumbai => "https://mumbai.polygonscan.com/tx/",
EthereumNetwork.MoonbeamMainnet => "https://blockscout.moonbeam.network/tx/",
EthereumNetwork.MoonbaseAlpha => "https://moonbase.moonscan.io/tx/",
EthereumNetwork.ArbitrumMainnetBeta => "https://explorer.arbitrum.io/tx/",
EthereumNetwork.ArbitrumRinkeby => "https://testnet.arbiscan.io/tx/",
EthereumNetwork.ArbitrumGoerli => "https://nitro-devnet-explorer.arbitrum.io/tx/",
EthereumNetwork.AvalancheMainnet => "https://snowtrace.io/tx/",
EthereumNetwork.AvalancheFuji => "https://testnet.snowtrace.io/tx/",
EthereumNetwork.SolanaDevnet => "https://explorer.solana.com/tx/",
EthereumNetwork.SolanaTestnet => "https://explorer.solana.com/tx/",
EthereumNetwork.SolanaMainnet => "https://explorer.solana.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(EthereumNetwork chain, string baseUrl, string txHash)
{
return chain switch
{
EthereumNetwork.SolanaDevnet => baseUrl + txHash + "?cluster=devnet",
EthereumNetwork.SolanaTestnet => baseUrl + txHash + "?cluster=testnet",
EthereumNetwork.SolanaMainnet => baseUrl + txHash,
_ => baseUrl + txHash,
};
}
}

View File

@ -4,5 +4,12 @@ namespace NftFaucet.Services;
public interface ISolanaTransactionService public interface ISolanaTransactionService
{ {
Task<string> MintNft(EthereumNetwork chain, string destinationAddress, string tokenUri, string name, double amount); Task<string> MintNft(EthereumNetwork chain,
string destinationAddress,
string tokenUri,
string name,
string symbol,
bool isTokenMutable,
uint sellerFeeBasisPoints,
ulong amount);
} }

View File

@ -22,7 +22,7 @@ public class SolanaTransactionInstructionsPipeline
Add(SystemProgram.Transfer(from, from, tokenPrice)); Add(SystemProgram.Transfer(from, from, tokenPrice));
} }
public void AddMetadata(PublicKey from, PublicKey mint, PublicKey metadataAddress, MetadataParameters data) public void AddMetadata(PublicKey from, PublicKey mint, PublicKey metadataAddress, MetadataParameters data, bool isMutable = true)
{ {
Add(MetadataProgram.CreateMetadataAccount( Add(MetadataProgram.CreateMetadataAccount(
metadataAddress, metadataAddress,
@ -32,7 +32,7 @@ public class SolanaTransactionInstructionsPipeline
from, from,
data, data,
true, true,
true isMutable
)); ));
} }

View File

@ -19,7 +19,9 @@ public class SolanaTransactionService : ISolanaTransactionService
string tokenUri, string tokenUri,
string name, string name,
string symbol, string symbol,
double amount) bool isTokenMutable,
uint sellerFeeBasisPoints,
ulong amount)
{ {
var cluster = chain switch var cluster = chain switch
{ {
@ -61,10 +63,10 @@ public class SolanaTransactionService : ISolanaTransactionService
var data = new MetadataParameters() var data = new MetadataParameters()
{ {
name = name, name = name,
symbol = "DNFT", symbol = symbol,
uri = tokenUri, uri = tokenUri,
creators = new List<Creator> { new Creator(walletAddress, 100, true) }, creators = new List<Creator> { new Creator(walletAddress, 100, true) },
sellerFeeBasisPoints = 88, sellerFeeBasisPoints = sellerFeeBasisPoints,
}; };
var destinationPublicKey = new PublicKey(destinationAddress); var destinationPublicKey = new PublicKey(destinationAddress);
@ -72,7 +74,7 @@ public class SolanaTransactionService : ISolanaTransactionService
var pipeline = new SolanaTransactionInstructionsPipeline(); var pipeline = new SolanaTransactionInstructionsPipeline();
pipeline.InitializeForMint(walletAddress, destinationPublicKey, mintAddress, rentExemption.Result, (ulong)amount, tokenPrice); pipeline.InitializeForMint(walletAddress, destinationPublicKey, mintAddress, rentExemption.Result, (ulong)amount, tokenPrice);
pipeline.AddMetadata(walletAddress, mintAddress, metadataAddress, data); pipeline.AddMetadata(walletAddress, mintAddress, metadataAddress, data, isTokenMutable);
pipeline.AddMasterEdition(walletAddress, mintAddress, masterEditionAddress, metadataAddress, data); pipeline.AddMasterEdition(walletAddress, mintAddress, masterEditionAddress, metadataAddress, data);
var blockHash = (await client.GetRecentBlockHashAsync()).Result.Value.Blockhash; var blockHash = (await client.GetRecentBlockHashAsync()).Result.Value.Blockhash;

View File

@ -1,15 +1,17 @@
@inherits MainLayoutComponent @using NftFaucet.Models.Enums
@inherits MainLayoutComponent
<Layout Style="background-color: #FFFFFF; height: 100%;"> <Layout Style="background-color: #FFFFFF; height: 100%;">
<Content Style="padding: 0 50px; display: flex; flex-direction: column;"> <Content Style="padding: 0 50px; display: flex; flex-direction: column;">
<PageHeader Class="create-nft-header"> <PageHeader Class="create-nft-header">
<PageHeaderTitle>NFT Faucet</PageHeaderTitle> <PageHeaderTitle>NFT Faucet</PageHeaderTitle>
<PageHeaderTags> <PageHeaderTags>
<Tag PresetColor="@PresetColor.Blue">Address: @(AppState?.Metamask?.Address ?? "<null>")</Tag> <Tag PresetColor="@PresetColor.Blue">Address: @(AppState.Storage.NetworkType == NetworkType.Ethereum ? AppState?.Metamask?.Address ?? "<null>" : "<null>")</Tag>
<Tag PresetColor="@ChainColor">@("Chain: " + (AppState?.Metamask?.Network?.ToString() ?? "<unknown>") + " (" + (AppState?.Metamask?.ChainId.ToString() ?? "<null>") + ")")</Tag> <Tag PresetColor="@ChainColor">@("Chain: " + (AppState.Storage.NetworkType == NetworkType.Ethereum ? AppState?.Metamask?.Network?.ToString() ?? "<unknown>" : AppState.Storage.Network.ToString()) + " (" + (AppState.Storage.NetworkType == NetworkType.Ethereum ? AppState?.Metamask?.ChainId.ToString() ?? "<null>" : "<null>") + ")")</Tag>
</PageHeaderTags> </PageHeaderTags>
</PageHeader> </PageHeader>
<Steps Current="@(AppState.Navigation.CurrentStep - 1)" Type="navigation" Class="fix-alignment"> <Steps Current="@(AppState.Navigation.CurrentStep - 1)" Type="navigation" Class="fix-alignment">
<Step Title="Select network type"/>
<Step Title="Specify token metadata"/> <Step Title="Specify token metadata"/>
<Step Title="Review metadata details"/> <Step Title="Review metadata details"/>
<Step Title="Review mint details"/> <Step Title="Review mint details"/>

View File

@ -6,7 +6,7 @@ namespace NftFaucet.Shared;
public class MainLayoutComponent : LayoutBasicComponent public class MainLayoutComponent : LayoutBasicComponent
{ {
private const int StepsCount = 4; private const int StepsCount = 5;
protected bool IsFirstStep => AppState.Navigation.CurrentStep == 1; protected bool IsFirstStep => AppState.Navigation.CurrentStep == 1;
protected bool IsLastStep => AppState.Navigation.CurrentStep == StepsCount; protected bool IsLastStep => AppState.Navigation.CurrentStep == StepsCount;
@ -15,15 +15,16 @@ public class MainLayoutComponent : LayoutBasicComponent
protected string ForwardButtonText => AppState.Navigation.CurrentStep switch protected string ForwardButtonText => AppState.Navigation.CurrentStep switch
{ {
1 => "Review NFT", 1 => "Confirm network selection",
2 => "Review mint", 2 => "Review NFT",
3 => "Send me this NFT!", 3 => "Review mint",
4 => "Send me this NFT!",
_ => "Next" _ => "Next"
}; };
protected string ForwardButtonIcon => AppState.Navigation.CurrentStep switch protected string ForwardButtonIcon => AppState.Navigation.CurrentStep switch
{ {
3 => "send", 4 => "send",
_ => "arrow-right", _ => "arrow-right",
}; };