1- import { useState , useCallback } from "react" ;
2- import { Upload as UploadIcon , Copy , Check , ExternalLink , AlertCircle } from "lucide-react" ;
1+ import { useState , useCallback , useEffect , useRef } from "react" ;
2+ import { Upload as UploadIcon , Copy , Check , ExternalLink , AlertCircle , FileText , Shield } from "lucide-react" ;
33import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/Card" ;
44import { Button } from "@/components/ui/Button" ;
55import { Textarea } from "@/components/ui/Textarea" ;
@@ -18,9 +18,15 @@ import { FileUpload } from "@/components/FileUpload";
1818import { AuthorizationCard } from "@/components/AuthorizationCard" ;
1919import { useApi , useClient } from "@/state/chain.state" ;
2020import { useSelectedAccount } from "@/state/wallet.state" ;
21- import { useAuthorization } from "@/state/storage.state" ;
21+ import {
22+ useAuthorization ,
23+ usePreimageAuth ,
24+ usePreimageAuthLoading ,
25+ checkPreimageAuthorization ,
26+ clearPreimageAuth ,
27+ } from "@/state/storage.state" ;
2228import { formatBytes } from "@/utils/format" ;
23- import { cidFromBytes , toHashingEnum } from "@/lib/cid" ;
29+ import { cidFromBytes , toHashingEnum , getContentHash } from "@/lib/cid" ;
2430import { Binary } from "polkadot-api" ;
2531
2632type HashAlgorithm = "blake2b256" | "sha256" | "keccak256" ;
@@ -41,13 +47,16 @@ interface UploadResult {
4147 blockHash ?: string ;
4248 blockNumber ?: number ;
4349 size : number ;
50+ unsigned ?: boolean ;
4451}
4552
4653export function Upload ( ) {
4754 const api = useApi ( ) ;
4855 const client = useClient ( ) ;
4956 const selectedAccount = useSelectedAccount ( ) ;
5057 const authorization = useAuthorization ( ) ;
58+ const preimageAuth = usePreimageAuth ( ) ;
59+ const preimageAuthLoading = usePreimageAuthLoading ( ) ;
5160
5261 const [ inputMode , setInputMode ] = useState < "text" | "file" > ( "text" ) ;
5362 const [ textData , setTextData ] = useState ( "" ) ;
@@ -62,6 +71,8 @@ export function Upload() {
6271 const [ uploadResult , setUploadResult ] = useState < UploadResult | null > ( null ) ;
6372 const [ copied , setCopied ] = useState ( false ) ;
6473
74+ const debounceTimer = useRef < ReturnType < typeof setTimeout > > ( undefined ) ;
75+
6576 const getData = useCallback ( ( ) : Uint8Array | null => {
6677 if ( inputMode === "text" ) {
6778 if ( ! textData . trim ( ) ) return null ;
@@ -72,14 +83,57 @@ export function Upload() {
7283
7384 const dataSize = getData ( ) ?. length ?? 0 ;
7485
75- const canUpload =
76- api &&
86+ // Check preimage authorization when data or hash algorithm changes
87+ useEffect ( ( ) => {
88+ if ( debounceTimer . current ) {
89+ clearTimeout ( debounceTimer . current ) ;
90+ }
91+
92+ const data = getData ( ) ;
93+ if ( ! data || ! api ) {
94+ clearPreimageAuth ( ) ;
95+ return ;
96+ }
97+
98+ debounceTimer . current = setTimeout ( async ( ) => {
99+ const hashConfig = HASH_ALGORITHMS . find ( h => h . value === hashAlgorithm ) ;
100+ if ( ! hashConfig ) return ;
101+
102+ try {
103+ const contentHash = await getContentHash ( data , hashConfig . mhCode ) ;
104+ await checkPreimageAuthorization ( api , contentHash ) ;
105+ } catch ( err ) {
106+ console . error ( "Failed to check preimage authorization:" , err ) ;
107+ }
108+ } , 300 ) ;
109+
110+ return ( ) => {
111+ if ( debounceTimer . current ) {
112+ clearTimeout ( debounceTimer . current ) ;
113+ }
114+ } ;
115+ } , [ api , inputMode , textData , fileData , hashAlgorithm , getData ] ) ;
116+
117+ const hasAccountAuth =
77118 selectedAccount &&
78119 authorization &&
79- dataSize > 0 &&
80120 authorization . bytes >= BigInt ( dataSize ) &&
81121 authorization . transactions > 0n ;
82122
123+ const hasPreimageAuth =
124+ preimageAuth &&
125+ preimageAuth . bytes >= BigInt ( dataSize ) &&
126+ preimageAuth . transactions > 0n ;
127+
128+ const canUpload =
129+ api &&
130+ client &&
131+ dataSize > 0 &&
132+ ( hasAccountAuth || hasPreimageAuth ) ;
133+
134+ // Preimage auth is preferred (same as pallet behavior)
135+ const willUseUnsigned = ! ! hasPreimageAuth ;
136+
83137 const handleFileSelect = useCallback ( ( file : File | null , data : Uint8Array | null ) => {
84138 setFileData ( data ) ;
85139 setFileName ( file ?. name ?? null ) ;
@@ -88,11 +142,15 @@ export function Upload() {
88142 } , [ ] ) ;
89143
90144 const handleUpload = async ( ) => {
91- if ( ! api || ! selectedAccount || ! client ) return ;
145+ if ( ! api || ! client ) return ;
92146
93147 const data = getData ( ) ;
94148 if ( ! data ) return ;
95149
150+ // Need either preimage auth or account auth with wallet
151+ if ( ! hasPreimageAuth && ! hasAccountAuth ) return ;
152+ if ( ! hasPreimageAuth && ! selectedAccount ) return ;
153+
96154 setIsUploading ( true ) ;
97155 setUploadError ( null ) ;
98156 setUploadResult ( null ) ;
@@ -123,35 +181,53 @@ export function Upload() {
123181 data : Binary . fromBytes ( data ) ,
124182 } ) ;
125183
126- // Sign and submit
184+ const useUnsigned = hasPreimageAuth ;
185+
127186 const result = await new Promise < { blockHash ?: string ; blockNumber ?: number } > ( ( resolve , reject ) => {
128187 let resolved = false ;
129188
130- const subscription = tx . signSubmitAndWatch ( selectedAccount . polkadotSigner ) . subscribe ( {
131- next : ( ev : any ) => {
132- console . log ( "TX event:" , ev . type ) ;
133- if ( ev . type === "txBestBlocksState" && ev . found && ! resolved ) {
134- resolved = true ;
135- subscription . unsubscribe ( ) ;
136- resolve ( {
137- blockHash : ev . block . hash ,
138- blockNumber : ev . block . number ,
139- } ) ;
140- }
141- } ,
142- error : ( err : any ) => {
143- if ( ! resolved ) {
144- resolved = true ;
145- reject ( err ) ;
146- }
147- } ,
148- } ) ;
189+ const handleEvent = ( ev : any ) => {
190+ console . log ( "TX event:" , ev . type ) ;
191+ if ( ev . type === "txBestBlocksState" && ev . found && ! resolved ) {
192+ resolved = true ;
193+ subscription . unsubscribe ( ) ;
194+ resolve ( {
195+ blockHash : ev . block . hash ,
196+ blockNumber : ev . block . number ,
197+ } ) ;
198+ }
199+ } ;
200+
201+ const handleError = ( err : any ) => {
202+ if ( ! resolved ) {
203+ resolved = true ;
204+ reject ( err ) ;
205+ }
206+ } ;
207+
208+ let subscription : { unsubscribe : ( ) => void } ;
209+
210+ if ( useUnsigned ) {
211+ // Unsigned submission via bareTx
212+ tx . getBareTx ( ) . then ( ( bareTx ) => {
213+ subscription = client . submitAndWatch ( bareTx ) . subscribe ( {
214+ next : handleEvent ,
215+ error : handleError ,
216+ } ) ;
217+ } ) . catch ( handleError ) ;
218+ } else {
219+ // Signed submission
220+ subscription = tx . signSubmitAndWatch ( selectedAccount ! . polkadotSigner ) . subscribe ( {
221+ next : handleEvent ,
222+ error : handleError ,
223+ } ) ;
224+ }
149225
150226 // Timeout after 2 minutes
151227 setTimeout ( ( ) => {
152228 if ( ! resolved ) {
153229 resolved = true ;
154- subscription . unsubscribe ( ) ;
230+ subscription ? .unsubscribe ( ) ;
155231 reject ( new Error ( "Transaction timed out" ) ) ;
156232 }
157233 } , 120000 ) ;
@@ -162,6 +238,7 @@ export function Upload() {
162238 blockHash : result . blockHash ,
163239 blockNumber : result . blockNumber ,
164240 size : data . length ,
241+ unsigned : ! ! useUnsigned ,
165242 } ) ;
166243 } catch ( err ) {
167244 console . error ( "Upload failed:" , err ) ;
@@ -295,12 +372,14 @@ export function Upload() {
295372 { isUploading ? (
296373 < >
297374 < Spinner size = "sm" className = "mr-2" />
298- Uploading...
375+ { willUseUnsigned ? " Uploading (unsigned) ..." : "Uploading..." }
299376 </ >
300377 ) : (
301378 < >
302379 < UploadIcon className = "h-5 w-5 mr-2" />
303- Upload to Bulletin Chain
380+ { willUseUnsigned
381+ ? "Upload to Bulletin Chain (unsigned)"
382+ : "Upload to Bulletin Chain" }
304383 </ >
305384 ) }
306385 </ Button >
@@ -324,7 +403,12 @@ export function Upload() {
324403 { uploadResult && (
325404 < Card className = "border-success" >
326405 < CardHeader >
327- < CardTitle className = "text-success" > Upload Successful</ CardTitle >
406+ < CardTitle className = "text-success flex items-center gap-2" >
407+ Upload Successful
408+ { uploadResult . unsigned && (
409+ < Badge variant = "secondary" > Unsigned</ Badge >
410+ ) }
411+ </ CardTitle >
328412 < CardDescription >
329413 Your data has been stored on the Bulletin Chain
330414 </ CardDescription >
@@ -382,13 +466,56 @@ export function Upload() {
382466
383467 { /* Sidebar */ }
384468 < div className = "space-y-6" >
469+ { /* Preimage Authorization Card */ }
470+ { dataSize > 0 && (
471+ < Card className = { hasPreimageAuth ? "border-green-500/50" : undefined } >
472+ < CardHeader >
473+ < CardTitle className = "flex items-center gap-2" >
474+ < FileText className = "h-5 w-5" />
475+ Preimage Authorization
476+ </ CardTitle >
477+ < CardDescription >
478+ No wallet required for pre-authorized data
479+ </ CardDescription >
480+ </ CardHeader >
481+ < CardContent >
482+ { preimageAuthLoading ? (
483+ < div className = "flex items-center justify-center h-16" >
484+ < Spinner size = "sm" />
485+ </ div >
486+ ) : hasPreimageAuth ? (
487+ < div className = "space-y-3" >
488+ < div className = "flex items-center gap-2" >
489+ < Badge variant = "default" className = "bg-green-600" > Authorized</ Badge >
490+ < span className = "text-sm text-muted-foreground" >
491+ Up to { formatBytes ( preimageAuth ! . bytes ) }
492+ </ span >
493+ </ div >
494+ { preimageAuth ! . expiresAt && (
495+ < p className = "text-xs text-muted-foreground" >
496+ Expires at block #{ preimageAuth ! . expiresAt }
497+ </ p >
498+ ) }
499+ < p className = "text-xs text-muted-foreground" >
500+ This data can be uploaded without a wallet connection
501+ </ p >
502+ </ div >
503+ ) : (
504+ < div className = "text-center text-muted-foreground py-2" >
505+ < p className = "text-sm" > No preimage authorization for this data</ p >
506+ </ div >
507+ ) }
508+ </ CardContent >
509+ </ Card >
510+ ) }
511+
385512 < AuthorizationCard />
386513
387- { ! selectedAccount && (
514+ { ! selectedAccount && ! hasPreimageAuth && (
388515 < Card >
389516 < CardContent className = "pt-6" >
390517 < div className = "text-center text-muted-foreground" >
391- < p className = "mb-4" > Connect a wallet to upload data</ p >
518+ < p className = "mb-4" > Connect a wallet or use pre-authorized data to upload </ p >
392519 < Button variant = "outline" asChild >
393520 < a href = { `${ import . meta. env . BASE_URL } accounts` } > Connect Wallet</ a >
394521 </ Button >
@@ -397,7 +524,7 @@ export function Upload() {
397524 </ Card >
398525 ) }
399526
400- { selectedAccount && ! authorization && (
527+ { selectedAccount && ! authorization && ! hasPreimageAuth && (
401528 < Card >
402529 < CardContent className = "pt-6" >
403530 < div className = "text-center text-muted-foreground" >
@@ -410,6 +537,27 @@ export function Upload() {
410537 </ CardContent >
411538 </ Card >
412539 ) }
540+
541+ { /* Submission mode indicator */ }
542+ { canUpload && (
543+ < Card >
544+ < CardContent className = "pt-6" >
545+ < div className = "flex items-center gap-2 text-sm" >
546+ { willUseUnsigned ? (
547+ < >
548+ < FileText className = "h-4 w-4 text-green-600" />
549+ < span > Will submit as < strong > unsigned</ strong > transaction (preimage authorized)</ span >
550+ </ >
551+ ) : (
552+ < >
553+ < Shield className = "h-4 w-4 text-blue-600" />
554+ < span > Will submit as < strong > signed</ strong > transaction (account authorized)</ span >
555+ </ >
556+ ) }
557+ </ div >
558+ </ CardContent >
559+ </ Card >
560+ ) }
413561 </ div >
414562 </ div >
415563 </ div >
0 commit comments