From fdf603c664e9ae1d8fbc877f94dea875d6ccec6d Mon Sep 17 00:00:00 2001 From: Erdit Kurteshi Date: Thu, 12 Oct 2023 16:17:14 +0200 Subject: [PATCH 1/4] added verify for signed-message in react for browser wallets --- examples/react/components/Content.tsx | 115 ++++++++++++++++++++------ 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/examples/react/components/Content.tsx b/examples/react/components/Content.tsx index 9cb715922..386972667 100644 --- a/examples/react/components/Content.tsx +++ b/examples/react/components/Content.tsx @@ -4,7 +4,11 @@ import type { AccountView, CodeResult, } from "near-api-js/lib/providers/provider"; -import type { Transaction } from "@near-wallet-selector/core"; +import type { + SignedMessage, + SignMessageParams, + Transaction, +} from "@near-wallet-selector/core"; import { verifyFullKeyBelongsToUser } from "@near-wallet-selector/core"; import { verifySignature } from "@near-wallet-selector/core"; import BN from "bn.js"; @@ -103,6 +107,14 @@ const Content: React.FC = () => { useEffect(() => { // TODO: don't just fetch once; subscribe! getMessages().then(setMessages); + + const timeoutId = setTimeout(() => { + verifyMessageBrowserWallet(); + }, 500); + + return () => { + clearTimeout(timeoutId); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -224,6 +236,68 @@ const Content: React.FC = () => { } }; + const verifyMessage = async ( + message: SignMessageParams, + signedMessage: SignedMessage + ) => { + const verifiedSignature = verifySignature({ + message: message.message, + nonce: message.nonce, + recipient: message.recipient, + publicKey: signedMessage.publicKey, + signature: signedMessage.signature, + callbackUrl: message.callbackUrl, + }); + const verifiedFullKeyBelongsToUser = await verifyFullKeyBelongsToUser({ + publicKey: signedMessage.publicKey, + accountId: signedMessage.accountId, + network: selector.options.network, + }); + + const isMessageVerified = verifiedFullKeyBelongsToUser && verifiedSignature; + + const alertMessage = isMessageVerified + ? "Successfully verified" + : "Failed to verify"; + + alert( + `${alertMessage} signed message: '${ + message.message + }': \n ${JSON.stringify(signedMessage)}` + ); + }; + + const verifyMessageBrowserWallet = useCallback(async () => { + const urlParams = new URLSearchParams( + window.location.hash.substring(1) // skip the first char (#) + ); + const accId = urlParams.get("accountId") as string; + const publicKey = urlParams.get("publicKey") as string; + const signature = urlParams.get("signature") as string; + + if (!accId && !publicKey && !signature) { + return; + } + + const message: SignMessageParams = JSON.parse( + localStorage.getItem("message")! + ); + + const signedMessage = { + accountId: accId, + publicKey, + signature, + }; + + await verifyMessage(message, signedMessage); + + const url = new URL(location.href); + url.hash = ""; + url.search = ""; + window.history.replaceState({}, document.title, url); + localStorage.removeItem("message"); + }, []); + const handleSubmit = useCallback( async (e: Submitted) => { e.preventDefault(); @@ -266,6 +340,18 @@ const Content: React.FC = () => { const nonce = Buffer.from(Array.from(Array(32).keys())); const recipient = "guest-book.testnet"; + if (wallet.type === "browser") { + localStorage.setItem( + "message", + JSON.stringify({ + message, + nonce: [...nonce], + recipient, + callbackUrl: location.href, + }) + ); + } + try { const signedMessage = await wallet.signMessage({ message, @@ -273,32 +359,7 @@ const Content: React.FC = () => { recipient, }); if (signedMessage) { - const verifiedSignature = verifySignature({ - message, - nonce, - recipient, - publicKey: signedMessage.publicKey, - signature: signedMessage.signature, - }); - const verifiedFullKeyBelongsToUser = await verifyFullKeyBelongsToUser({ - publicKey: signedMessage.publicKey, - accountId: signedMessage.accountId, - network: selector.options.network, - }); - - if (verifiedFullKeyBelongsToUser && verifiedSignature) { - alert( - `Successfully verify signed message: '${message}': \n ${JSON.stringify( - signedMessage - )}` - ); - } else { - alert( - `Failed to verify signed message '${message}': \n ${JSON.stringify( - signedMessage - )}` - ); - } + await verifyMessage({ message, nonce, recipient }, signedMessage); } } catch (err) { const errMsg = From c554b1a931701b5390eb82d126ea071b37b46d16 Mon Sep 17 00:00:00 2001 From: Erdit Kurteshi Date: Fri, 13 Oct 2023 13:40:58 +0200 Subject: [PATCH 2/4] added verify signed message alert for browser wallets --- .../components/content/content.component.ts | 111 +++++++++++++----- 1 file changed, 84 insertions(+), 27 deletions(-) diff --git a/examples/angular/src/app/components/content/content.component.ts b/examples/angular/src/app/components/content/content.component.ts index c4ab8f5c6..59201d92c 100644 --- a/examples/angular/src/app/components/content/content.component.ts +++ b/examples/angular/src/app/components/content/content.component.ts @@ -5,7 +5,12 @@ import type { AccountView, CodeResult, } from "near-api-js/lib/providers/provider"; -import type { AccountState, Transaction } from "@near-wallet-selector/core"; +import type { + AccountState, + SignMessageParams, + SignedMessage, + Transaction, +} from "@near-wallet-selector/core"; import { verifyFullKeyBelongsToUser, verifySignature, @@ -48,6 +53,8 @@ export class ContentComponent implements OnInit, OnDestroy { this.getAccount(), ]); + this.verifyMessageBrowserWallet(); + this.account = account; this.messages = messages; @@ -165,12 +172,87 @@ export class ContentComponent implements OnInit, OnDestroy { } } + async verifyMessage( + message: SignMessageParams, + signedMessage: SignedMessage + ) { + const verifiedSignature = verifySignature({ + message: message.message, + nonce: message.nonce, + recipient: message.recipient, + publicKey: signedMessage.publicKey, + signature: signedMessage.signature, + callbackUrl: message.callbackUrl, + }); + const verifiedFullKeyBelongsToUser = await verifyFullKeyBelongsToUser({ + publicKey: signedMessage.publicKey, + accountId: signedMessage.accountId, + network: this.selector.options.network, + }); + + const isMessageVerified = verifiedFullKeyBelongsToUser && verifiedSignature; + + const alertMessage = isMessageVerified + ? "Successfully verified" + : "Failed to verify"; + + alert( + `${alertMessage} signed message: '${ + message.message + }': \n ${JSON.stringify(signedMessage)}` + ); + } + + verifyMessageBrowserWallet() { + const urlParams = new URLSearchParams( + window.location.hash.substring(1) // skip the first char (#) + ); + + const accId = urlParams.get("accountId") as string; + const publicKey = urlParams.get("publicKey") as string; + const signature = urlParams.get("signature") as string; + + if (!accId && !publicKey && !signature) { + return; + } + + const message: SignMessageParams = JSON.parse( + localStorage.getItem("message") as string + ); + + const signedMessage = { + accountId: accId, + publicKey, + signature, + }; + + this.verifyMessage(message, signedMessage); + + const url = new URL(location.href); + url.hash = ""; + url.search = ""; + window.history.replaceState({}, document.title, url); + localStorage.removeItem("message"); + } + async onSignMessage() { const wallet = await this.selector.wallet(); const message = "test message to sign"; const nonce = Buffer.from(Array.from(Array(32).keys())); const recipient = "guest-book.testnet"; + if (wallet.type === "browser") { + localStorage.setItem( + "message", + JSON.stringify({ + message, + nonce: [...nonce], + recipient, + callbackUrl: location.href, + }) + ); + } + try { const signedMessage = await wallet.signMessage({ message, @@ -178,32 +260,7 @@ export class ContentComponent implements OnInit, OnDestroy { recipient, }); if (signedMessage) { - const verifiedSignature = verifySignature({ - message, - nonce, - recipient, - publicKey: signedMessage.publicKey, - signature: signedMessage.signature, - }); - const verifiedFullKeyBelongsToUser = await verifyFullKeyBelongsToUser({ - publicKey: signedMessage.publicKey, - accountId: signedMessage.accountId, - network: this.selector.options.network, - }); - - if (verifiedFullKeyBelongsToUser && verifiedSignature) { - alert( - `Successfully verify signed message: '${message}': \n ${JSON.stringify( - signedMessage - )}` - ); - } else { - alert( - `Failed to verify signed message '${message}': \n ${JSON.stringify( - signedMessage - )}` - ); - } + await this.verifyMessage({ message, nonce, recipient }, signedMessage); } } catch (err) { const errMsg = From d2a4c36801192f7eebd6004b28eaac37f6b0270e Mon Sep 17 00:00:00 2001 From: Erdit Kurteshi Date: Fri, 13 Oct 2023 14:21:12 +0200 Subject: [PATCH 3/4] added changes to resolve comments --- .../src/app/components/content/content.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/angular/src/app/components/content/content.component.ts b/examples/angular/src/app/components/content/content.component.ts index 59201d92c..631c027da 100644 --- a/examples/angular/src/app/components/content/content.component.ts +++ b/examples/angular/src/app/components/content/content.component.ts @@ -53,12 +53,11 @@ export class ContentComponent implements OnInit, OnDestroy { this.getAccount(), ]); - this.verifyMessageBrowserWallet(); - this.account = account; this.messages = messages; this.subscribeToEvents(); + await this.verifyMessageBrowserWallet(); } async getAccountBalance({ provider, accountId }: GetAccountBalanceProps) { @@ -203,7 +202,7 @@ export class ContentComponent implements OnInit, OnDestroy { ); } - verifyMessageBrowserWallet() { + async verifyMessageBrowserWallet() { const urlParams = new URLSearchParams( window.location.hash.substring(1) // skip the first char (#) ); @@ -226,7 +225,7 @@ export class ContentComponent implements OnInit, OnDestroy { signature, }; - this.verifyMessage(message, signedMessage); + await this.verifyMessage(message, signedMessage); const url = new URL(location.href); url.hash = ""; From 15636cb660895458d27fc3dbdcf636be8153addf Mon Sep 17 00:00:00 2001 From: Erdit Kurteshi Date: Fri, 13 Oct 2023 14:40:53 +0200 Subject: [PATCH 4/4] fix eslint warning --- examples/react/components/Content.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/react/components/Content.tsx b/examples/react/components/Content.tsx index 386972667..68d60c6c6 100644 --- a/examples/react/components/Content.tsx +++ b/examples/react/components/Content.tsx @@ -296,6 +296,7 @@ const Content: React.FC = () => { url.search = ""; window.history.replaceState({}, document.title, url); localStorage.removeItem("message"); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSubmit = useCallback(