Skip to content

Commit dc2d9a7

Browse files
committed
Add unsigned transaction upload support via preimage authorization
1 parent 9384398 commit dc2d9a7

File tree

2 files changed

+228
-37
lines changed

2 files changed

+228
-37
lines changed

console-ui/src/pages/Upload/Upload.tsx

Lines changed: 183 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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";
33
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/Card";
44
import { Button } from "@/components/ui/Button";
55
import { Textarea } from "@/components/ui/Textarea";
@@ -18,9 +18,15 @@ import { FileUpload } from "@/components/FileUpload";
1818
import { AuthorizationCard } from "@/components/AuthorizationCard";
1919
import { useApi, useClient } from "@/state/chain.state";
2020
import { 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";
2228
import { formatBytes } from "@/utils/format";
23-
import { cidFromBytes, toHashingEnum } from "@/lib/cid";
29+
import { cidFromBytes, toHashingEnum, getContentHash } from "@/lib/cid";
2430
import { Binary } from "polkadot-api";
2531

2632
type HashAlgorithm = "blake2b256" | "sha256" | "keccak256";
@@ -41,13 +47,16 @@ interface UploadResult {
4147
blockHash?: string;
4248
blockNumber?: number;
4349
size: number;
50+
unsigned?: boolean;
4451
}
4552

4653
export 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

Comments
 (0)