Add Mint dialog

This commit is contained in:
Ivan Yaremenchuk 2022-09-29 21:54:11 -05:00
parent 4a0cbb6c66
commit 6795de500f
12 changed files with 361 additions and 162 deletions

View File

@ -0,0 +1,7 @@
namespace NftFaucetRadzen.Models;
public class Balance
{
public decimal Amount { get; set; }
public string Currency { get; set; }
}

View File

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

View File

@ -0,0 +1,93 @@
@page "/mint/in-progress"
@using NftFaucetRadzen.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>
@if (!string.IsNullOrEmpty(SourceAddress))
{
@:<text style="color: green;">@SourceAddress</text>
}
else
{
@:<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>
@if (Balance != null)
{
@:<text style="color: red;">@(Balance.Amount + " " + Balance.Currency)</text>
}
else
{
@:<text style="color: red;">&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/>
<text style="font-weight: bold;">Transaction hash: </text>
<text style="color: green;">@TransactionHash</text>
</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 NftFaucetRadzen.Components;
using NftFaucetRadzen.Models;
using NftFaucetRadzen.Plugins.NetworkPlugins;
using NftFaucetRadzen.Utils;
namespace NftFaucetRadzen.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

@ -26,34 +26,6 @@
{ {
@:<text style="color: red;">NOT CONFIGURED</text> @:<text style="color: red;">NOT CONFIGURED</text>
} }
else if (!NetworkMatches)
{
@:<text style="color: red;">NETWORK MISMATCH</text>
} else
{
@:<text style="color: green;">OK</text>
}
</p>
<p>
@if (string.IsNullOrEmpty(SourceAddress))
{
@:<text style="font-weight: bold;">Balance: </text>
}
else
{
@:<text style="font-weight: bold;">Balance</text>
@:<text> (@SourceAddress)</text>
@:<text style="font-weight: bold;">: </text>
}
@if (AppState?.SelectedProvider == null || !AppState.SelectedProvider.IsConfigured || string.IsNullOrEmpty(SourceAddress))
{
@:<text style="color: red;">UNKNOWN</text>
}
else if (BalanceIsZero)
{
@:<text style="color: red;">ZERO</text>
}
else else
{ {
@:<text style="color: green;">OK</text> @:<text style="color: green;">OK</text>

View File

@ -1,6 +1,5 @@
using CSharpFunctionalExtensions; using CSharpFunctionalExtensions;
using NftFaucetRadzen.Components; using NftFaucetRadzen.Components;
using NftFaucetRadzen.Models;
using NftFaucetRadzen.Utils; using NftFaucetRadzen.Utils;
using Radzen; using Radzen;
@ -9,8 +8,6 @@ namespace NftFaucetRadzen.Pages;
public partial class MintPage : BasicComponent public partial class MintPage : BasicComponent
{ {
private string SourceAddress { get; set; } private string SourceAddress { get; set; }
private bool NetworkMatches { get; set; }
private bool BalanceIsZero { get; set; } = true;
private bool IsReadyToMint => AppState != null && private bool IsReadyToMint => AppState != null &&
AppState.SelectedNetwork != null && AppState.SelectedNetwork != null &&
AppState.SelectedProvider != null && AppState.SelectedProvider != null &&
@ -18,9 +15,7 @@ public partial class MintPage : BasicComponent
AppState.SelectedContract != null && AppState.SelectedContract != null &&
AppState.SelectedToken != null && AppState.SelectedToken != null &&
AppState.SelectedUploadLocation != null && AppState.SelectedUploadLocation != null &&
!string.IsNullOrEmpty(SourceAddress) && AppState.UserStorage.DestinationAddress != null;
NetworkMatches &&
!BalanceIsZero;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -28,35 +23,13 @@ public partial class MintPage : BasicComponent
{ {
SourceAddress = await ResultWrapper.Wrap(() => AppState.SelectedProvider.GetAddress()).Match(x => x, _ => null); SourceAddress = await ResultWrapper.Wrap(() => AppState.SelectedProvider.GetAddress()).Match(x => x, _ => null);
AppState.UserStorage.DestinationAddress = SourceAddress; AppState.UserStorage.DestinationAddress = SourceAddress;
if (string.IsNullOrEmpty(SourceAddress) || AppState.SelectedNetwork == null)
{
BalanceIsZero = true;
}
else
{
var balance = await ResultWrapper.Wrap(() => AppState.SelectedProvider.GetBalance(AppState.SelectedNetwork)).Match(x => x, _ => 0);
BalanceIsZero = balance == 0;
}
if (AppState.SelectedNetwork != null)
{
NetworkMatches = await ResultWrapper.Wrap(() => AppState.SelectedProvider.EnsureNetworkMatches(AppState.SelectedNetwork)).Match(x => x, _ => false);
}
} }
} }
private async Task Mint() private async Task Mint()
{ {
var mintRequest = new MintRequest(AppState.SelectedNetwork, AppState.SelectedProvider, await DialogService.OpenAsync<MintDialog>("Minting...",
AppState.SelectedContract, AppState.SelectedToken, AppState.SelectedUploadLocation, new Dictionary<string, object>(),
AppState.UserStorage.DestinationAddress, AppState.UserStorage.TokenAmount); new DialogOptions() { Width = "700px", Height = "570px", Resizable = true, Draggable = true });
var result = await AppState.SelectedProvider.Mint(mintRequest);
if (result.IsSuccess)
{
NotificationService.Notify(NotificationSeverity.Success, "Minting finished", result.Value);
}
else
{
NotificationService.Notify(NotificationSeverity.Error, "Failed to mint", result.Error);
}
} }
} }

View File

@ -1,4 +1,3 @@
using CSharpFunctionalExtensions;
using NftFaucetRadzen.Components.CardList; using NftFaucetRadzen.Components.CardList;
using NftFaucetRadzen.Models; using NftFaucetRadzen.Models;
using NftFaucetRadzen.Plugins.NetworkPlugins; using NftFaucetRadzen.Plugins.NetworkPlugins;
@ -19,9 +18,9 @@ public interface IProvider
public CardListItemConfiguration GetConfiguration(); public CardListItemConfiguration GetConfiguration();
public bool IsNetworkSupported(INetwork network); public bool IsNetworkSupported(INetwork network);
public Task<string> GetAddress(); public Task<string> GetAddress();
public Task<long?> GetBalance(INetwork network); public Task<Balance> GetBalance(INetwork network);
public Task<bool> EnsureNetworkMatches(INetwork network); public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork);
public Task<Result<string>> Mint(MintRequest mintRequest); public Task<string> Mint(MintRequest mintRequest);
public Task<string> GetState(); public Task<string> GetState();
public Task SetState(string state); public Task SetState(string state);
} }

View File

@ -85,7 +85,7 @@ public class EthereumKeygenProvider : IProvider
public Task<string> GetAddress() public Task<string> GetAddress()
=> Task.FromResult(Key?.Address); => Task.FromResult(Key?.Address);
public async Task<long?> GetBalance(INetwork network) public async Task<Balance> GetBalance(INetwork network)
{ {
if (string.IsNullOrEmpty(Key?.Address)) if (string.IsNullOrEmpty(Key?.Address))
return null; return null;
@ -93,13 +93,24 @@ public class EthereumKeygenProvider : IProvider
var web3 = new Web3(network.PublicRpcUrl.OriginalString); var web3 = new Web3(network.PublicRpcUrl.OriginalString);
var hexBalance = await web3.Eth.GetBalance.SendRequestAsync(Key.Address); var hexBalance = await web3.Eth.GetBalance.SendRequestAsync(Key.Address);
var balance = (long) hexBalance.Value; var balance = (long) hexBalance.Value;
return balance; return new Balance
{
Amount = balance,
Currency = "wei",
};
} }
public Task<bool> EnsureNetworkMatches(INetwork network) public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
=> Task.FromResult(network.Type == NetworkType.Ethereum); {
if (selectedNetwork != null && selectedNetwork.Type == NetworkType.Ethereum)
return Task.FromResult(selectedNetwork);
public async Task<Result<string>> Mint(MintRequest mintRequest) var matchingNetwork = allKnownNetworks.FirstOrDefault(x => x.Type == NetworkType.Ethereum && x.IsSupported) ??
allKnownNetworks.FirstOrDefault(x => x.Type == NetworkType.Ethereum);
return Task.FromResult(matchingNetwork);
}
public async Task<string> Mint(MintRequest mintRequest)
{ {
if (mintRequest.Network.Type != NetworkType.Ethereum) if (mintRequest.Network.Type != NetworkType.Ethereum)
{ {
@ -123,8 +134,6 @@ public class EthereumKeygenProvider : IProvider
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
return await ResultWrapper.Wrap(async () =>
{
var data = transfer.Encode(); var data = transfer.Encode();
var client = new RpcClient(mintRequest.Network.PublicRpcUrl); var client = new RpcClient(mintRequest.Network.PublicRpcUrl);
var account = new Account(Key.PrivateKey); var account = new Account(Key.PrivateKey);
@ -150,7 +159,6 @@ public class EthereumKeygenProvider : IProvider
} }
return transactionHash; return transactionHash;
});
} }
public Task<string> GetState() public Task<string> GetState()

View File

@ -101,7 +101,7 @@ public class SolanaKeygenProvider : IProvider
public Task<string> GetAddress() public Task<string> GetAddress()
=> Task.FromResult(Key?.Address); => Task.FromResult(Key?.Address);
public async Task<long?> GetBalance(INetwork network) public async Task<Balance> GetBalance(INetwork network)
{ {
if (string.IsNullOrEmpty(Key?.Address)) if (string.IsNullOrEmpty(Key?.Address))
return null; return null;
@ -112,13 +112,24 @@ public class SolanaKeygenProvider : IProvider
return null; return null;
var balance = (long) balanceResult.Result.Value; var balance = (long) balanceResult.Result.Value;
return balance; return new Balance
{
Amount = balance,
Currency = "lamport",
};
} }
public Task<bool> EnsureNetworkMatches(INetwork network) public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
=> Task.FromResult(network.Type == NetworkType.Solana); {
if (selectedNetwork != null && selectedNetwork.Type == NetworkType.Solana)
return Task.FromResult(selectedNetwork);
public async Task<Result<string>> Mint(MintRequest mintRequest) var matchingNetwork = allKnownNetworks.FirstOrDefault(x => x.Type == NetworkType.Solana && x.IsSupported) ??
allKnownNetworks.FirstOrDefault(x => x.Type == NetworkType.Solana);
return Task.FromResult(matchingNetwork);
}
public async Task<string> Mint(MintRequest mintRequest)
{ {
var client = ClientFactory.GetClient(mintRequest.Network.PublicRpcUrl.OriginalString); var client = ClientFactory.GetClient(mintRequest.Network.PublicRpcUrl.OriginalString);
var rentExemption = await client.GetMinimumBalanceForRentExemptionAsync(TokenProgram.MintAccountDataSize); var rentExemption = await client.GetMinimumBalanceForRentExemptionAsync(TokenProgram.MintAccountDataSize);

View File

@ -133,70 +133,50 @@ public class MetamaskProvider : IProvider
public async Task<string> GetAddress() public async Task<string> GetAddress()
=> Address ?? await MetaMaskService.GetSelectedAccountAsync(); => Address ?? await MetaMaskService.GetSelectedAccountAsync();
public async Task<long?> GetBalance(INetwork network) public async Task<Balance> GetBalance(INetwork network)
{ {
if (!IsConfigured) if (!IsConfigured)
return null; return null;
var balance = (long) await MetaMaskService.GetBalanceAsync(); var balance = (long) await MetaMaskService.GetBalanceAsync();
return balance; return new Balance
}
public async Task<bool> EnsureNetworkMatches(INetwork network)
=> network.Type == NetworkType.Ethereum && network.ChainId != null && network.ChainId == await GetChainId();
private async Task<ulong?> GetChainId()
{ {
var chainHex = await MetaMaskService.GetSelectedChainAsync(); Amount = balance,
if (string.IsNullOrEmpty(chainHex) || chainHex == "0x") Currency = "wei",
};
}
public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
{ {
return null; var chainId = Convert.ToUInt64(ChainId, 16);
var matchingNetwork = allKnownNetworks.FirstOrDefault(x => x.ChainId != null && x.ChainId.Value == chainId);
return Task.FromResult(matchingNetwork);
} }
var chainId = (ulong) Convert.ToInt64(chainHex, 16); public async Task<string> Mint(MintRequest mintRequest)
return chainId;
}
public async Task<Result<string>> Mint(MintRequest mintRequest)
{ {
if (mintRequest.Network.Type != NetworkType.Ethereum) if (mintRequest.Network.Type != NetworkType.Ethereum)
{ {
throw new InvalidOperationException("Invalid network type for this provider"); throw new InvalidOperationException("Invalid network type for this provider");
} }
switch (mintRequest.Contract.Type) Function transfer = mintRequest.Contract.Type switch
{ {
case ContractType.Erc721: ContractType.Erc721 => new Erc721MintFunction
{
return await ResultWrapper.Wrap(async () =>
{
var contractAddress = mintRequest.Contract.Address;
var transfer = new Erc721MintFunction
{ {
To = mintRequest.DestinationAddress, To = mintRequest.DestinationAddress,
Uri = mintRequest.UploadLocation.Location, Uri = mintRequest.UploadLocation.Location,
}; },
var data = transfer.Encode(); ContractType.Erc1155 => new Erc1155MintFunction
var transactionHash = await MetaMaskService.SendTransactionAsync(contractAddress, 0, data);
if (string.IsNullOrEmpty(transactionHash))
{
throw new Exception("Operation was cancelled or RPC node failure");
}
return transactionHash;
});
}
case ContractType.Erc1155:
{
return await ResultWrapper.Wrap(async () =>
{
var contractAddress = mintRequest.Contract.Address;
var transfer = new Erc1155MintFunction
{ {
To = mintRequest.DestinationAddress, To = mintRequest.DestinationAddress,
Amount = mintRequest.TokensAmount, Amount = mintRequest.TokensAmount,
Uri = mintRequest.UploadLocation.Location, Uri = mintRequest.UploadLocation.Location,
},
_ => throw new ArgumentOutOfRangeException(),
}; };
var contractAddress = mintRequest.Contract.Address;
var data = transfer.Encode(); var data = transfer.Encode();
var transactionHash = await MetaMaskService.SendTransactionAsync(contractAddress, 0, data); var transactionHash = await MetaMaskService.SendTransactionAsync(contractAddress, 0, data);
if (string.IsNullOrEmpty(transactionHash)) if (string.IsNullOrEmpty(transactionHash))
@ -205,11 +185,6 @@ public class MetamaskProvider : IProvider
} }
return transactionHash; return transactionHash;
});
}
default:
throw new ArgumentOutOfRangeException();
}
} }
public Task<string> GetState() public Task<string> GetState()

View File

@ -49,7 +49,12 @@ public class PhantomProvider : IProvider
public Task<string> GetAddress() public Task<string> GetAddress()
=> throw new NotImplementedException(); => throw new NotImplementedException();
public Task<long?> GetBalance(INetwork network) public Task<Balance> GetBalance(INetwork network)
{
throw new NotImplementedException();
}
public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -59,7 +64,7 @@ public class PhantomProvider : IProvider
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<Result<string>> Mint(MintRequest mintRequest) public Task<string> Mint(MintRequest mintRequest)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }