1
+ import { ed25519 } from "@noble/curves/ed25519" ;
2
+ import { PublicKey } from "@solana/web3.js" ;
3
+ import bs58 from 'bs58' ;
4
+ import dynamic from 'next/dynamic' ;
5
+ import { SetStateAction , useCallback , useEffect , useMemo , useState } from "react" ;
6
+
7
+ import { MeteredMessageBox } from "./MeteredMessageBox" ;
8
+ import { SigningContext , SignMessageBox } from "./SignMessageButton" ;
9
+
10
+ const ConnectButton = dynamic ( async ( ) => ( ( await import ( '@solana/wallet-adapter-react-ui' ) ) . WalletMultiButton ) , { ssr : false } ) ;
11
+
12
+ export type ReportMessageVerification = ( verified : boolean , show ?: boolean , message ?: string ) => void ;
13
+
14
+ const MAX_MSG_LENGTH = 1500 ;
15
+
16
+ function getPluralizedWord ( count : number ) : string {
17
+ return count === 1 ? "character" : "characters" ;
18
+ }
19
+
20
+ function sanitizeInput ( input : string ) {
21
+ input = input . replace ( / < s c r i p t .* ?> .* ?< \/ s c r i p t > / gi, '' ) ;
22
+ if ( input . length > MAX_MSG_LENGTH ) {
23
+ console . log ( "Message length limit reached. Truncating..." ) ;
24
+ input = input . substring ( 0 , MAX_MSG_LENGTH ) ;
25
+ }
26
+ return input ;
27
+ }
28
+
29
+ export const MessageForm = ( props : { reportVerification : ReportMessageVerification } ) => {
30
+ const [ address , setAddress ] = useState ( "" ) ;
31
+ const [ message , setMessage ] = useState ( "" ) ;
32
+ const [ signature , setSignature ] = useState ( "" ) ;
33
+ const [ addressError , setAddressError ] = useState ( false ) ;
34
+ const [ verified , setVerifiedInternal ] = useState ( false ) ;
35
+
36
+ const setVerified = useCallback ( ( verified : boolean , show = false , message = "" ) => {
37
+ setVerifiedInternal ( verified ) ;
38
+ props . reportVerification ( verified , show , message ) ;
39
+ } , [ props ] ) ;
40
+
41
+ const handleAddressChange = useCallback ( ( event : { target : { value : SetStateAction < string > ; } ; } ) => {
42
+ setVerified ( false ) ;
43
+ const update = event . target . value ;
44
+ setAddress ( update ) ;
45
+
46
+ try {
47
+ let isError = false ;
48
+ if ( update . length > 0 && ! PublicKey . isOnCurve ( update ) ) {
49
+ isError = true ;
50
+ }
51
+ setAddressError ( isError ) ;
52
+ } catch ( error : unknown ) {
53
+ if ( error instanceof Error ) {
54
+ console . error ( error . message ) ;
55
+ }
56
+ setAddressError ( true ) ;
57
+ }
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ } , [ ] ) ;
60
+
61
+ const handleSignatureChange = useCallback ( ( event : { target : { value : SetStateAction < string > ; } ; } ) => {
62
+ setVerified ( false ) ;
63
+ setSignature ( event . target . value ) ;
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ } , [ ] ) ;
66
+
67
+ const handleInputChange = useCallback ( ( event : { target : { value : string ; } ; } ) => {
68
+ setVerified ( false ) ;
69
+ setMessage ( sanitizeInput ( event . target . value ) ) ;
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ } , [ ] ) ;
72
+
73
+ const handleVerifyClick = useCallback ( ( ) => {
74
+ try {
75
+ const verified = ed25519 . verify ( bs58 . decode ( signature ) , new TextEncoder ( ) . encode ( message ) , bs58 . decode ( address ) ) ;
76
+ if ( ! verified ) throw new Error ( "Message verification failed!" ) ;
77
+ setVerified ( true )
78
+ } catch ( error ) {
79
+ console . error ( "Message verification failed!" ) ;
80
+ setVerified ( false , true ) ;
81
+ }
82
+ } , [ setVerified , address , message , signature ] ) ;
83
+
84
+ useEffect ( ( ) => {
85
+ const urlParams = new URLSearchParams ( window . location . search ) ;
86
+ const urlAddress = urlParams . get ( 'address' ) ;
87
+ const urlMessage = urlParams . get ( 'message' ) ;
88
+ const urlSignature = urlParams . get ( 'signature' ) ;
89
+
90
+ if ( urlAddress && urlMessage && urlSignature ) {
91
+ handleAddressChange ( { target : { value : urlAddress } } ) ;
92
+ handleInputChange ( { target : { value : urlMessage } } ) ;
93
+ handleSignatureChange ( { target : { value : urlSignature } } ) ;
94
+ }
95
+ } , [ handleAddressChange , handleInputChange , handleSignatureChange ] ) ;
96
+
97
+ const signingContext = useMemo ( ( ) => {
98
+ return {
99
+ address,
100
+ input : message ,
101
+ setAddress : handleAddressChange ,
102
+ setInput : handleInputChange ,
103
+ setSignature : handleSignatureChange ,
104
+ setVerified,
105
+ signature,
106
+ } as SigningContext ;
107
+ } , [ message , address , signature , handleAddressChange , handleSignatureChange , handleInputChange , setVerified ] ) ;
108
+
109
+ function writeToClipboard ( ) {
110
+ const encodedAddress = encodeURIComponent ( address ) ;
111
+ const encodedMessage = encodeURIComponent ( message ) ;
112
+ const encodedSignature = encodeURIComponent ( signature ) ;
113
+ const newUrl = `${ window . location . origin } ${ window . location . pathname } ?address=${ encodedAddress } &message=${ encodedMessage } &signature=${ encodedSignature } ` ;
114
+ navigator . clipboard . writeText ( newUrl ) . catch ( err => {
115
+ console . error ( "Failed to copy to clipboard: " , err ) ;
116
+ } ) ;
117
+ }
118
+
119
+ const placeholder_message = 'Type a message here...' ;
120
+ const placeholder_address = 'Enter an address whose signature you want to verify...' ;
121
+ const placeholder_signature = 'Paste a signature...' ;
122
+ const verifyButtonDisabled = ! address || ! message || ! signature ;
123
+
124
+ return (
125
+ < div className = "card" >
126
+ < div className = "card-header" style = { { padding : '2.5rem 1.5rem' } } >
127
+ < div className = "row align-items-center d-flex justify-content-between" >
128
+ < div className = "col" >
129
+ < h2 className = "card-header-title" > Message Signer</ h2 >
130
+ </ div >
131
+ < div className = "col-auto" >
132
+ < ConnectButton style = { {
133
+ borderRadius : '0.5rem' ,
134
+ } } />
135
+ </ div >
136
+ </ div >
137
+ </ div >
138
+ < div className = "card-header" >
139
+ < h3 className = "card-header-title" > Address</ h3 >
140
+ </ div >
141
+ < div className = "card-body" >
142
+ < textarea
143
+ rows = { 2 }
144
+ onChange = { handleAddressChange }
145
+ value = { address }
146
+ className = "form-control form-control-auto"
147
+ placeholder = { placeholder_address }
148
+ />
149
+ { addressError && (
150
+ < div className = "text-warning small mt-2" >
151
+ < i className = "fe fe-alert-circle" > </ i > Invalid address.
152
+ </ div >
153
+ ) }
154
+ </ div >
155
+ < div className = "card-header" >
156
+ < h3 className = "card-header-title" > Message</ h3 >
157
+ </ div >
158
+ < div className = "card-body" >
159
+ < MeteredMessageBox
160
+ value = { message }
161
+ onChange = { handleInputChange }
162
+ placeholder = { placeholder_message }
163
+ word = { getPluralizedWord ( MAX_MSG_LENGTH - message . length ) }
164
+ limit = { MAX_MSG_LENGTH }
165
+ count = { message . length }
166
+ charactersremaining = { MAX_MSG_LENGTH - message . length } />
167
+ </ div >
168
+
169
+ < div className = "card-header" >
170
+ < h3 className = "card-header-title" > Signature</ h3 >
171
+ </ div >
172
+ < div className = "card-body" >
173
+ < div style = { { display : 'flex' , flexDirection : 'column' , flexGrow : 1 , overflowY : 'auto' } } >
174
+ < textarea
175
+ rows = { 2 }
176
+ onChange = { handleSignatureChange }
177
+ value = { signature }
178
+ className = "form-control form-control-auto"
179
+ placeholder = { placeholder_signature }
180
+ />
181
+ </ div >
182
+ </ div >
183
+
184
+ < div className = "card-footer d-flex justify-content-end" >
185
+ < div className = "me-2" data-bs-toggle = "tooltip" data-bs-placement = "top" title = { ! verified ? "Verify first to enable this action" : "" } >
186
+ < button
187
+ className = "btn btn-primary"
188
+ onClick = { writeToClipboard }
189
+ disabled = { ! verified }
190
+ >
191
+ Copy URL
192
+ </ button >
193
+ </ div >
194
+ < div className = "me-2" data-bs-toggle = "tooltip" data-bs-placement = "top" title = { verifyButtonDisabled ? "Complete the form to enable this action" : "" } >
195
+ < button
196
+ className = "btn btn-primary"
197
+ onClick = { handleVerifyClick }
198
+ disabled = { verifyButtonDisabled }
199
+ >
200
+ Verify
201
+ </ button >
202
+ </ div >
203
+ < SignMessageBox className = "btn btn-primary me-2" signingcontext = { signingContext } />
204
+ </ div >
205
+ </ div >
206
+ ) ;
207
+ } ;
0 commit comments