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

@ -8,17 +8,17 @@
<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>

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>
}
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
{
@:<text style="color: green;">OK</text>

View File

@ -1,6 +1,5 @@
using CSharpFunctionalExtensions;
using NftFaucetRadzen.Components;
using NftFaucetRadzen.Models;
using NftFaucetRadzen.Utils;
using Radzen;
@ -9,8 +8,6 @@ namespace NftFaucetRadzen.Pages;
public partial class MintPage : BasicComponent
{
private string SourceAddress { get; set; }
private bool NetworkMatches { get; set; }
private bool BalanceIsZero { get; set; } = true;
private bool IsReadyToMint => AppState != null &&
AppState.SelectedNetwork != null &&
AppState.SelectedProvider != null &&
@ -18,9 +15,7 @@ public partial class MintPage : BasicComponent
AppState.SelectedContract != null &&
AppState.SelectedToken != null &&
AppState.SelectedUploadLocation != null &&
!string.IsNullOrEmpty(SourceAddress) &&
NetworkMatches &&
!BalanceIsZero;
AppState.UserStorage.DestinationAddress != null;
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);
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()
{
var mintRequest = new MintRequest(AppState.SelectedNetwork, AppState.SelectedProvider,
AppState.SelectedContract, AppState.SelectedToken, AppState.SelectedUploadLocation,
AppState.UserStorage.DestinationAddress, AppState.UserStorage.TokenAmount);
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);
}
await DialogService.OpenAsync<MintDialog>("Minting...",
new Dictionary<string, object>(),
new DialogOptions() { Width = "700px", Height = "570px", Resizable = true, Draggable = true });
}
}

View File

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

View File

@ -85,7 +85,7 @@ public class EthereumKeygenProvider : IProvider
public Task<string> GetAddress()
=> Task.FromResult(Key?.Address);
public async Task<long?> GetBalance(INetwork network)
public async Task<Balance> GetBalance(INetwork network)
{
if (string.IsNullOrEmpty(Key?.Address))
return null;
@ -93,13 +93,24 @@ public class EthereumKeygenProvider : IProvider
var web3 = new Web3(network.PublicRpcUrl.OriginalString);
var hexBalance = await web3.Eth.GetBalance.SendRequestAsync(Key.Address);
var balance = (long) hexBalance.Value;
return balance;
return new Balance
{
Amount = balance,
Currency = "wei",
};
}
public Task<bool> EnsureNetworkMatches(INetwork network)
=> Task.FromResult(network.Type == NetworkType.Ethereum);
public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
{
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)
{
@ -123,34 +134,31 @@ public class EthereumKeygenProvider : IProvider
_ => throw new ArgumentOutOfRangeException(),
};
return await ResultWrapper.Wrap(async () =>
var data = transfer.Encode();
var client = new RpcClient(mintRequest.Network.PublicRpcUrl);
var account = new Account(Key.PrivateKey);
var transactionManager = new AccountSignerTransactionManager(client, account);
var txInput = new TransactionInput
{
var data = transfer.Encode();
var client = new RpcClient(mintRequest.Network.PublicRpcUrl);
var account = new Account(Key.PrivateKey);
var transactionManager = new AccountSignerTransactionManager(client, account);
var txInput = new TransactionInput
{
From = Key.Address,
To = mintRequest.Contract.Address,
Data = data,
ChainId = new HexBigInteger(new BigInteger(mintRequest.Network.ChainId!.Value)),
Type = new HexBigInteger(TransactionType.EIP1559.AsByte()),
};
txInput.Nonce = await transactionManager.GetNonceAsync(txInput);
var fee1559 = await transactionManager.CalculateFee1559Async();
txInput.MaxFeePerGas = new HexBigInteger(fee1559.MaxFeePerGas!.Value);
txInput.MaxPriorityFeePerGas = new HexBigInteger(fee1559.MaxPriorityFeePerGas!.Value);
txInput.Gas = await transactionManager.EstimateGasAsync(txInput);
txInput.Gas = new HexBigInteger(new BigInteger((long)txInput.Gas.Value * 1.3));
var transactionHash = await transactionManager.SendTransactionAsync(txInput);
if (string.IsNullOrEmpty(transactionHash))
{
throw new Exception("Operation was cancelled or RPC node failure");
}
From = Key.Address,
To = mintRequest.Contract.Address,
Data = data,
ChainId = new HexBigInteger(new BigInteger(mintRequest.Network.ChainId!.Value)),
Type = new HexBigInteger(TransactionType.EIP1559.AsByte()),
};
txInput.Nonce = await transactionManager.GetNonceAsync(txInput);
var fee1559 = await transactionManager.CalculateFee1559Async();
txInput.MaxFeePerGas = new HexBigInteger(fee1559.MaxFeePerGas!.Value);
txInput.MaxPriorityFeePerGas = new HexBigInteger(fee1559.MaxPriorityFeePerGas!.Value);
txInput.Gas = await transactionManager.EstimateGasAsync(txInput);
txInput.Gas = new HexBigInteger(new BigInteger((long)txInput.Gas.Value * 1.3));
var transactionHash = await transactionManager.SendTransactionAsync(txInput);
if (string.IsNullOrEmpty(transactionHash))
{
throw new Exception("Operation was cancelled or RPC node failure");
}
return transactionHash;
});
return transactionHash;
}
public Task<string> GetState()

View File

@ -101,7 +101,7 @@ public class SolanaKeygenProvider : IProvider
public Task<string> GetAddress()
=> Task.FromResult(Key?.Address);
public async Task<long?> GetBalance(INetwork network)
public async Task<Balance> GetBalance(INetwork network)
{
if (string.IsNullOrEmpty(Key?.Address))
return null;
@ -112,13 +112,24 @@ public class SolanaKeygenProvider : IProvider
return null;
var balance = (long) balanceResult.Result.Value;
return balance;
return new Balance
{
Amount = balance,
Currency = "lamport",
};
}
public Task<bool> EnsureNetworkMatches(INetwork network)
=> Task.FromResult(network.Type == NetworkType.Solana);
public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
{
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 rentExemption = await client.GetMinimumBalanceForRentExemptionAsync(TokenProgram.MintAccountDataSize);

View File

@ -133,83 +133,58 @@ public class MetamaskProvider : IProvider
public async Task<string> GetAddress()
=> Address ?? await MetaMaskService.GetSelectedAccountAsync();
public async Task<long?> GetBalance(INetwork network)
public async Task<Balance> GetBalance(INetwork network)
{
if (!IsConfigured)
return null;
var balance = (long) await MetaMaskService.GetBalanceAsync();
return 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();
if (string.IsNullOrEmpty(chainHex) || chainHex == "0x")
return new Balance
{
return null;
}
var chainId = (ulong) Convert.ToInt64(chainHex, 16);
return chainId;
Amount = balance,
Currency = "wei",
};
}
public async Task<Result<string>> Mint(MintRequest mintRequest)
public Task<INetwork> GetNetwork(IReadOnlyCollection<INetwork> allKnownNetworks, INetwork selectedNetwork)
{
var chainId = Convert.ToUInt64(ChainId, 16);
var matchingNetwork = allKnownNetworks.FirstOrDefault(x => x.ChainId != null && x.ChainId.Value == chainId);
return Task.FromResult(matchingNetwork);
}
public async Task<string> Mint(MintRequest mintRequest)
{
if (mintRequest.Network.Type != NetworkType.Ethereum)
{
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,
Uri = mintRequest.UploadLocation.Location,
};
var data = transfer.Encode();
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:
To = mintRequest.DestinationAddress,
Uri = mintRequest.UploadLocation.Location,
},
ContractType.Erc1155 => new Erc1155MintFunction
{
return await ResultWrapper.Wrap(async () =>
{
var contractAddress = mintRequest.Contract.Address;
var transfer = new Erc1155MintFunction
{
To = mintRequest.DestinationAddress,
Amount = mintRequest.TokensAmount,
Uri = mintRequest.UploadLocation.Location,
};
var data = transfer.Encode();
var transactionHash = await MetaMaskService.SendTransactionAsync(contractAddress, 0, data);
if (string.IsNullOrEmpty(transactionHash))
{
throw new Exception("Operation was cancelled or RPC node failure");
}
To = mintRequest.DestinationAddress,
Amount = mintRequest.TokensAmount,
Uri = mintRequest.UploadLocation.Location,
},
_ => throw new ArgumentOutOfRangeException(),
};
return transactionHash;
});
}
default:
throw new ArgumentOutOfRangeException();
var contractAddress = mintRequest.Contract.Address;
var data = transfer.Encode();
var transactionHash = await MetaMaskService.SendTransactionAsync(contractAddress, 0, data);
if (string.IsNullOrEmpty(transactionHash))
{
throw new Exception("Operation was cancelled or RPC node failure");
}
return transactionHash;
}
public Task<string> GetState()

View File

@ -49,7 +49,12 @@ public class PhantomProvider : IProvider
public Task<string> GetAddress()
=> 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();
}
@ -59,7 +64,7 @@ public class PhantomProvider : IProvider
throw new NotImplementedException();
}
public Task<Result<string>> Mint(MintRequest mintRequest)
public Task<string> Mint(MintRequest mintRequest)
{
throw new NotImplementedException();
}