diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index e9c95f22..4edbcfa7 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -15,6 +15,7 @@ export function Navbar() { const homePath = useClusterPath({ pathname: '/' }); const supplyPath = useClusterPath({ pathname: '/supply' }); const inspectorPath = useClusterPath({ pathname: '/tx/inspector' }); + const messagePath = useClusterPath({ pathname: '/message' }); const selectedLayoutSegment = useSelectedLayoutSegment(); const selectedLayoutSegments = useSelectedLayoutSegments(); return ( @@ -48,16 +49,26 @@ export function Navbar() {
  • Inspector
  • +
  • + + Message + +
  • diff --git a/app/components/message/MessageContext.tsx b/app/components/message/MessageContext.tsx new file mode 100644 index 00000000..38012db3 --- /dev/null +++ b/app/components/message/MessageContext.tsx @@ -0,0 +1,33 @@ +import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; +import { + ConnectionProvider, + WalletProvider, +} from "@solana/wallet-adapter-react"; +import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; +import { UnsafeBurnerWalletAdapter } from '@solana/wallet-adapter-wallets'; +import { clusterApiUrl } from "@solana/web3.js"; +import React, { FC, ReactNode, useMemo } from "react"; + +export const SignerWalletContext: FC<{ children: ReactNode }> = ({ children }) => { + // Always use devnet, this page never needs to use RPC + const network = WalletAdapterNetwork.Devnet; + const endpoint = useMemo(() => clusterApiUrl(network), [network]); + + const wallets = useMemo( + () => [ + new UnsafeBurnerWalletAdapter(), + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [network] + ); + + return ( + + + + {children} + + + + ); +}; diff --git a/app/components/message/MessageForm.tsx b/app/components/message/MessageForm.tsx new file mode 100644 index 00000000..ae53eb2d --- /dev/null +++ b/app/components/message/MessageForm.tsx @@ -0,0 +1,209 @@ +import { ed25519 } from "@noble/curves/ed25519"; +import { PublicKey } from "@solana/web3.js"; +import bs58 from 'bs58'; +import dynamic from 'next/dynamic'; +import { SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; + +import { MeteredMessageBox } from "./MeteredMessageBox"; +import { SIGNING_DOMAIN, SigningContext, SignMessageBox } from "./SignMessageButton"; + +const ConnectButton = dynamic(async () => ((await import('@solana/wallet-adapter-react-ui')).WalletMultiButton), { ssr: false }); + +export type ReportMessageVerification = (verified: boolean, show?: boolean, message?: string) => void; + +const MAX_MSG_LENGTH = 1500; + +function getPluralizedWord(count: number): string { + return count === 1 ? "character" : "characters"; +} + +function sanitizeInput(input: string) { + input = input.replace(/.*?<\/script>/gi, ''); + if (input.length > MAX_MSG_LENGTH) { + console.log("Message length limit reached. Truncating..."); + input = input.substring(0, MAX_MSG_LENGTH); + } + return input; +} + +export const MessageForm = (props: { reportVerification: ReportMessageVerification }) => { + const [address, setAddress] = useState(""); + const [message, setMessage] = useState(""); + const [signature, setSignature] = useState(""); + const [addressError, setAddressError] = useState(false); + const [verified, setVerifiedInternal] = useState(false); + + const setVerified = useCallback((verified: boolean, show = false, message = "") => { + setVerifiedInternal(verified); + props.reportVerification(verified, show, message); + }, [props]); + + const handleAddressChange = useCallback((event: { target: { value: SetStateAction; }; }) => { + setVerified(false); + const update = event.target.value; + setAddress(update); + + try { + let isError = false; + if (update.length > 0 && !PublicKey.isOnCurve(update)) { + isError = true; + } + setAddressError(isError); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(error.message); + } + setAddressError(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSignatureChange = useCallback((event: { target: { value: SetStateAction; }; }) => { + setVerified(false); + setSignature(event.target.value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleInputChange = useCallback((event: { target: { value: string; }; }) => { + setVerified(false); + setMessage(sanitizeInput(event.target.value)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleVerifyClick = useCallback(() => { + try { + const messageBytes = new TextEncoder().encode(SIGNING_DOMAIN + message); + const verified = ed25519.verify(bs58.decode(signature), messageBytes, bs58.decode(address)); + if (!verified) throw new Error("Message verification failed!"); + setVerified(true) + } catch (error) { + console.error("Message verification failed!"); + setVerified(false, true); + } + }, [setVerified, address, message, signature]); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const urlAddress = urlParams.get('address'); + const urlMessage = urlParams.get('message'); + const urlSignature = urlParams.get('signature'); + + if (urlAddress && urlMessage && urlSignature) { + handleAddressChange({ target: { value: urlAddress } }); + handleInputChange({ target: { value: urlMessage } }); + handleSignatureChange({ target: { value: urlSignature } }); + } + }, [handleAddressChange, handleInputChange, handleSignatureChange]); + + const signingContext = useMemo(() => { + return { + address, + input: message, + setAddress: handleAddressChange, + setInput: handleInputChange, + setSignature: handleSignatureChange, + setVerified, + signature, + } as SigningContext; + }, [message, address, signature, handleAddressChange, handleSignatureChange, handleInputChange, setVerified]); + + function writeToClipboard() { + const encodedAddress = encodeURIComponent(address); + const encodedMessage = encodeURIComponent(message); + const encodedSignature = encodeURIComponent(signature); + const newUrl = `${window.location.origin}${window.location.pathname}?address=${encodedAddress}&message=${encodedMessage}&signature=${encodedSignature}`; + navigator.clipboard.writeText(newUrl).catch(err => { + console.error("Failed to copy to clipboard: ", err); + }); + } + + const placeholder_message = 'Type a message here...'; + const placeholder_address = 'Enter an address whose signature you want to verify...'; + const placeholder_signature = 'Paste a signature...'; + const verifyButtonDisabled = !address || !message || !signature; + + return ( +
    +
    +
    +
    +

    Message Signer

    +
    +
    + +
    +
    +
    +
    +

    Address

    +
    +
    +