From 4b4907ad731a05fc477d6b7d7a1a908481e3b02d Mon Sep 17 00:00:00 2001 From: David Hernando Date: Fri, 23 Aug 2024 13:22:27 +0200 Subject: [PATCH] Test Transfer CSPR functionality Signed-off-by: David Hernando --- .../Components/PublicKeyTextBox.razor.cs | 4 +- .../NCTLWebExplorer/Pages/BlockDetail.razor | 5 +- .../NCTLWebExplorer/Pages/BlockExplorer.razor | 5 +- .../NCTLWebExplorer/Pages/TransferCspr.razor | 21 ++- .../Pages/TransferCspr.razor.cs | 162 +++++++++++++----- .../NCTLWebExplorer/Pages/_Layout.cshtml | 32 ++-- Docs/Demos/NCTLWebExplorer/Program.cs | 3 + .../NCTLWebExplorer/Shared/MainLayout.razor | 1 + .../NCTLWebExplorer/Utils/CsprClickInterop.cs | 77 +++++++++ Docs/Demos/NCTLWebExplorer/wwwroot/app.js | 80 +++++++++ .../NCTLWebExplorer/wwwroot/css/site.css | 18 ++ 11 files changed, 340 insertions(+), 68 deletions(-) create mode 100644 Docs/Demos/NCTLWebExplorer/Utils/CsprClickInterop.cs create mode 100644 Docs/Demos/NCTLWebExplorer/wwwroot/app.js diff --git a/Docs/Demos/NCTLWebExplorer/Components/PublicKeyTextBox.razor.cs b/Docs/Demos/NCTLWebExplorer/Components/PublicKeyTextBox.razor.cs index 1edb9c3..b16c408 100644 --- a/Docs/Demos/NCTLWebExplorer/Components/PublicKeyTextBox.razor.cs +++ b/Docs/Demos/NCTLWebExplorer/Components/PublicKeyTextBox.razor.cs @@ -1,6 +1,4 @@ -using Casper.Network.SDK.Web; -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; +using Microsoft.AspNetCore.Components; using Radzen; namespace NCTLWebExplorer.Components; diff --git a/Docs/Demos/NCTLWebExplorer/Pages/BlockDetail.razor b/Docs/Demos/NCTLWebExplorer/Pages/BlockDetail.razor index de3c3ad..8a9e92c 100644 --- a/Docs/Demos/NCTLWebExplorer/Pages/BlockDetail.razor +++ b/Docs/Demos/NCTLWebExplorer/Pages/BlockDetail.razor @@ -1,4 +1,5 @@ @page "/blocks/{BlockHash}" +@using System.Globalization @inherits ExplorerComponent; @if (_block is null) @@ -23,9 +24,9 @@ else
Timestamp
@(_block.Timestamp)
Block Height
- @(_block.Height) + @(_block.Height.ToString("N0", CultureInfo.InvariantCulture))
Era Id
- @(_block.EraId) + @(_block.EraId.ToString("N0", CultureInfo.InvariantCulture))
Switch Block
@(_block.EraEnd != null ? "Yes" : "No") diff --git a/Docs/Demos/NCTLWebExplorer/Pages/BlockExplorer.razor b/Docs/Demos/NCTLWebExplorer/Pages/BlockExplorer.razor index 9c74618..5659f17 100644 --- a/Docs/Demos/NCTLWebExplorer/Pages/BlockExplorer.razor +++ b/Docs/Demos/NCTLWebExplorer/Pages/BlockExplorer.razor @@ -1,4 +1,5 @@ @page "/blocks" +@using System.Globalization @using Casper.Network.SDK.Types @using NCTLWebExplorer.Models @@ -19,12 +20,12 @@ diff --git a/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor b/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor index 01b2f6f..e9b55bc 100644 --- a/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor +++ b/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor @@ -1,5 +1,6 @@ -@page "/transfer-cspr" +@page "/transfer-cspr" @inherits ExplorerComponent; +@inject CsprClickInterop _csprClickInterop; Transfer $CSPR @@ -23,9 +24,19 @@
- +
Sender Public Key:
+ @if (!string.IsNullOrWhiteSpace(_originPublicKey)) + { +
+ +
+ } + else + { +
+ +
+ }
- + \ No newline at end of file diff --git a/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor.cs b/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor.cs index 029ba04..73eb8bc 100644 --- a/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor.cs +++ b/Docs/Demos/NCTLWebExplorer/Pages/TransferCspr.razor.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Casper.Network.SDK; using Casper.Network.SDK.JsonRpc; using Casper.Network.SDK.Types; @@ -11,55 +11,131 @@ public partial class TransferCspr { private string _originPublicKey; private string _targetPublicKey; - private string _transferAmount; + private string _transferAmount = "2.5"; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + await _csprClickInterop.SetDotNetInstance(); + Console.WriteLine("Getting key"); + _originPublicKey = await _csprClickInterop.GetActiveKey(); + } + catch (Exception e) + { + Console.WriteLine("Error getting key"); + Console.WriteLine(e.Message); + } + + _csprClickInterop.OnStateUpdate += (type, key) => + { + _originPublicKey = key; + StateHasChanged(); + }; + StateHasChanged(); + } + } + async Task SendTransferBtnClicked() { ErrorMessage = null; SuccessMessage = null; - // var state = await SignerInterop.GetState(); - // - // if (state is not {IsUnlocked: true}) - // { - // NotificationService.Notify(new NotificationMessage - // { - // Severity = NotificationSeverity.Error, Summary = "Unlock Casper Signer to sign the deploy", - // Duration = 4000 - // }); - // return; - // } - // - // var casperService = CasperRpcService as CasperRPCService; - // - // var deploy = DeployTemplates.StandardTransfer(PublicKey.FromHexString(state.ActivePK), - // PublicKey.FromHexString(_targetPublicKey), - // BigInteger.Parse(_transferAmount) * 1_000_000_000, - // new BigInteger(100_000_000), - // casperService?.ChainName, - // 1); - // - // deploy = await SignDeployWithSigner(deploy, state.ActivePK, _targetPublicKey); - // - // try - // { - // var rpcResponse = await CasperRpcService?.PutDeploy(deploy)!; - // var result = rpcResponse.Parse(); - // Console.WriteLine("RESULT: " + result.DeployHash); - // SuccessMessage = "Deploy hash: " + result.DeployHash; - // - // NotificationService.Notify(new NotificationMessage - // {Severity = NotificationSeverity.Success, Summary = "Transfer successfully sent.", Duration = 4000}); - // } - // catch (RpcClientException e) - // { - // this.ErrorMessage = e.Message; - // - // NotificationService.Notify(new NotificationMessage - // {Severity = NotificationSeverity.Error, Summary = "Error sending the transaction.", Duration = 4000}); - // } + if (string.IsNullOrWhiteSpace(_originPublicKey)) + { + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Error, Summary = "Sender account not valid.", + Duration = 4000 + }); + return; + } + + try + { + if (string.IsNullOrWhiteSpace(_targetPublicKey)) + throw new Exception(); + + if(_originPublicKey == _targetPublicKey) + throw new Exception(); + + var pk = PublicKey.FromHexString(_targetPublicKey); + } + catch (Exception e) + { + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Error, Summary = "Recipient must be a valid public key.", + Duration = 4000 + }); + return; + } + + BigInteger amount; + + try + { + if (string.IsNullOrWhiteSpace(_transferAmount)) + throw new Exception(); + + var cspr = float.Parse(_transferAmount); + if (cspr < 2.5) + throw new Exception(); + var motes = (ulong)(cspr * 1_000_000_000); + amount = new BigInteger(motes); + } + catch (Exception e) + { + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Error, Summary = "Amount not valid", + Duration = 4000 + }); + return; + } + + var casperService = CasperRpcService as CasperRPCService; + + var deploy = DeployTemplates.StandardTransfer( + PublicKey.FromHexString(_originPublicKey), + PublicKey.FromHexString(_targetPublicKey), + amount, + new BigInteger(100_000_000), + casperService?.ChainName, + 1); + + deploy = await _csprClickInterop.SignDeploy(deploy, _originPublicKey); + + if (deploy is null) + { + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Error, Summary = "Error getting the deploy signature.", + Duration = 4000 + }); + return; + } + + try + { + var rpcResponse = await CasperRpcService?.PutDeploy(deploy)!; + var result = rpcResponse.Parse(); + SuccessMessage = "Deploy hash: " + result.DeployHash; + + NotificationService.Notify(new NotificationMessage + {Severity = NotificationSeverity.Success, Summary = "Transfer successfully sent.", Duration = 4000}); + } + catch (RpcClientException e) + { + this.ErrorMessage = e.Message; + + NotificationService.Notify(new NotificationMessage + {Severity = NotificationSeverity.Error, Summary = "Error sending the transaction.", Duration = 4000}); + } _targetPublicKey = null; _transferAmount = null; } -} +} \ No newline at end of file diff --git a/Docs/Demos/NCTLWebExplorer/Pages/_Layout.cshtml b/Docs/Demos/NCTLWebExplorer/Pages/_Layout.cshtml index 58473bc..bc46b60 100644 --- a/Docs/Demos/NCTLWebExplorer/Pages/_Layout.cshtml +++ b/Docs/Demos/NCTLWebExplorer/Pages/_Layout.cshtml @@ -14,22 +14,28 @@ + + -@RenderBody() +
+
+
+
+ @RenderBody() -
- - An error has occurred. This application may no longer respond until reloaded. - - - An unhandled exception has occurred. See browser dev tools for details. - - Reload - 🗙 -
- - +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ +
\ No newline at end of file diff --git a/Docs/Demos/NCTLWebExplorer/Program.cs b/Docs/Demos/NCTLWebExplorer/Program.cs index bdb5fd1..b25d6b9 100644 --- a/Docs/Demos/NCTLWebExplorer/Program.cs +++ b/Docs/Demos/NCTLWebExplorer/Program.cs @@ -1,5 +1,6 @@ using NCTLWebExplorer.Services; using Casper.Network.SDK.Web; +using NCTLWebExplorer.Utils; using Radzen; var builder = WebApplication.CreateBuilder(args); @@ -16,6 +17,8 @@ builder.Services.AddCasperSSEService(builder.Configuration); builder.Services.AddSingleton(); +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/Docs/Demos/NCTLWebExplorer/Shared/MainLayout.razor b/Docs/Demos/NCTLWebExplorer/Shared/MainLayout.razor index d46ea4f..632c357 100644 --- a/Docs/Demos/NCTLWebExplorer/Shared/MainLayout.razor +++ b/Docs/Demos/NCTLWebExplorer/Shared/MainLayout.razor @@ -28,6 +28,7 @@ + diff --git a/Docs/Demos/NCTLWebExplorer/Utils/CsprClickInterop.cs b/Docs/Demos/NCTLWebExplorer/Utils/CsprClickInterop.cs new file mode 100644 index 0000000..07cfaf6 --- /dev/null +++ b/Docs/Demos/NCTLWebExplorer/Utils/CsprClickInterop.cs @@ -0,0 +1,77 @@ +using Casper.Network.SDK.Types; +using Casper.Network.SDK.Web; +using Microsoft.JSInterop; + +namespace NCTLWebExplorer.Utils; + +public delegate void CsprClickEventHandler(string eventType, string activePublicKey); + +public class CsprClickInterop +{ + private readonly ILogger _logger; + + private readonly IJSRuntime _jsRuntime; + + public event CsprClickEventHandler OnStateUpdate; + + public string ActivePK { get; set; } + + public CsprClickInterop(IJSRuntime jsRuntime, ILogger logger) + { + _jsRuntime = jsRuntime; + _logger = logger; + } + + private async Task _callCsprClickInterop(string method, params object[] par) + { + await _jsRuntime.InvokeVoidAsync(method, par); + } + + private async Task _callCsprClickInterop(string method, params object[] par) + { + return await _jsRuntime.InvokeAsync(method, par); + } + + public async Task SetDotNetInstance() + { + await _callCsprClickInterop("setDotNetInstance", + DotNetObjectReference.Create(this)); + } + + public async Task GetActiveKey() + { + Console.WriteLine("GetActiveKey"); + var key = await _callCsprClickInterop("csprclickGetActiveKey"); + Console.WriteLine("GetActiveKey 2"); + + return key; + } + + public async Task SignDeploy(Deploy deploy, string signingKey) + { + var json = deploy.SerializeToJson(); + var signature = await _callCsprClickInterop("csprclickSignDeploy", + json, signingKey); + if (signature != null) + { + deploy.AddApproval(new Approval() + { + Signer = PublicKey.FromHexString(signingKey), + Signature = Signature.FromHexString(signature), + }); + return deploy; + } + + return null; + } + + [JSInvokable("UpdateState")] + public void UpdateState(string eventType, string activePublicKey) + { + _logger.LogDebug("CSPR.click updated state: " + + $"ActivePK:{activePublicKey}"); + + ActivePK = activePublicKey; + OnStateUpdate?.Invoke(eventType, activePublicKey); + } +} \ No newline at end of file diff --git a/Docs/Demos/NCTLWebExplorer/wwwroot/app.js b/Docs/Demos/NCTLWebExplorer/wwwroot/app.js new file mode 100644 index 0000000..1ecb562 --- /dev/null +++ b/Docs/Demos/NCTLWebExplorer/wwwroot/app.js @@ -0,0 +1,80 @@ + +// Set up CSPR.click UI (Top Bar) +// +const uiContainer = 'csprclick-ui'; + +const defaultTheme = 'light'; + +const onThemeChanged = (theme) => { + const page = document.querySelector('body'); + if (theme === 'dark') page?.classList.add('dark'); + else page?.classList.remove('dark'); + console.log('Theme switched to', theme); +}; + +const accountMenuItems = [ + 'CopyHashMenuItem', +]; + +const clickUIOptions = { + uiContainer, + rootAppElement: '#app', + show1ClickModal: true, + showTopBar: true, + defaultTheme, + accountMenuItems, +}; + +const clickSDKOptions = { + appName: 'DevNet Explorer', + appId: 'bf1e3953-5513-4a66-a7a8-0b52f644', + providers: ['casper-wallet', 'ledger', 'casperdash'], +}; + +window.addEventListener('csprclick:loaded', () => { + window.csprclick.on('csprclick:signed_in', async (evt) => { + console.log("csprclick:signed_in", evt); + csprclickEventHandler("csprclick:signed_in", evt.account.public_key); + }); + window.csprclick.on('csprclick:switched_account', async (evt) => { + console.log("csprclick:switched_account", evt); + csprclickEventHandler("csprclick:switched_account", evt.account.public_key); + }); + window.csprclick.on('csprclick:signed_out', async (evt) => { + console.log("csprclick:signed_out", evt); + csprclickEventHandler("csprclick:signed_out", ""); + }); + window.csprclick.on('csprclick:disconnected', async (evt) => { + console.log("csprclick:disconnected", evt); + csprclickEventHandler("csprclick:disconnected", ""); + }); +}); + +var dotNetCsprClickInteropInstance; + +function setDotNetInstance(instance) { + dotNetCsprClickInteropInstance = instance; +} + +function csprclickEventHandler(eventType, publicKey) { + if (typeof dotNetCsprClickInteropInstance !== 'undefined') + dotNetCsprClickInteropInstance.invokeMethodAsync('UpdateState', + eventType, publicKey); +} + +async function csprclickGetActiveKey() { + console.log("csprclickGetActiveKey", "1"); + if(window.csprclick === undefined) return ""; + console.log("csprclickGetActiveKey", "2"); + var account = await window.csprclick.getActiveAccount(); + console.log("csprclickGetActiveKey", account); + return account.public_key; +} + +async function csprclickSignDeploy(deploy, signingKey) { + if(window.csprclick === undefined) return ""; + var result = await window.csprclick.sign(deploy, signingKey); + if(!result.cancelled && result.signatureHex) + return result.signatureHex; + return null; +} \ No newline at end of file diff --git a/Docs/Demos/NCTLWebExplorer/wwwroot/css/site.css b/Docs/Demos/NCTLWebExplorer/wwwroot/css/site.css index 4b0141c..5042ac7 100644 --- a/Docs/Demos/NCTLWebExplorer/wwwroot/css/site.css +++ b/Docs/Demos/NCTLWebExplorer/wwwroot/css/site.css @@ -1,4 +1,16 @@ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); +@import url('https://fonts.cdnfonts.com/css/inter'); + +@font-face { + font-family: 'JetBrains Mono'; + src: url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2') + format('woff2'), + url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff') + format('woff'); + font-weight: 400; + font-style: normal; + font-display: swap; +} html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; @@ -73,4 +85,10 @@ h3 span.gskey { div:has(> json-viewer) { overflow: scroll; +} + +div#csprclick-ui-wrapper { + background-color: rgb(24, 29, 64);; + padding-left: 13px; + padding-right: 16px; } \ No newline at end of file