From 2583a4b686a64f64d07b553404025e85bee5b939 Mon Sep 17 00:00:00 2001 From: tuanhqv123 Date: Sat, 17 Jan 2026 21:11:18 +0700 Subject: [PATCH 1/4] fix blod id --- app/marketplace/my-data/page.tsx | 876 ++++++++++++++++++------------- app/marketplace/page.tsx | 461 ++++++++++++---- hooks/useMarketplace.ts | 205 +++++++- 3 files changed, 1064 insertions(+), 478 deletions(-) diff --git a/app/marketplace/my-data/page.tsx b/app/marketplace/my-data/page.tsx index 82f6820..9fb1c0a 100644 --- a/app/marketplace/my-data/page.tsx +++ b/app/marketplace/my-data/page.tsx @@ -7,17 +7,9 @@ import { useAllListings, useAccountBalance } from '@/hooks/useMarketplace'; import { formatSize, bytesToHex, formatPrice } from '@/lib/marketplace'; import { getFullnodeUrl } from '@mysten/sui/client'; import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'; -import gsap from 'gsap'; -import type { Transaction } from '@mysten/sui/transactions'; -import { TransactionStatus, useTransactionSteps } from '@/components/marketplace/TransactionStatus'; - -// Type for Walrus write files flow -interface WalrusWriteFlow { - encode: () => Promise; - register: (opts: { epochs: number; owner: string; deletable: boolean }) => Transaction; - upload: (opts: { digest: string }) => Promise; - certify: () => Transaction; -} +import { Transaction } from '@mysten/sui/transactions'; +import { marketplaceConfig, MIST_PER_SUI } from '@/config/marketplace'; +import { getMarketplaceTarget } from '@/lib/marketplace'; const walrusModule = { walrus: null as null | typeof import('@mysten/walrus').walrus, @@ -31,6 +23,25 @@ async function getWalrus() { } type Tab = 'uploads' | 'purchases'; +type UploadStep = 'form' | 'processing' | 'complete'; +type Step = 'encode' | 'register' | 'upload' | 'certify' | 'listing' | 'complete'; + +interface TransactionLog { + step: string; + status: 'pending' | 'processing' | 'success' | 'error'; + message: string; + details?: string; + timestamp: string; +} + +const STEPS: { key: Step; label: string; icon: string }[] = [ + { key: 'encode', label: 'Encode', icon: 'encryption' }, + { key: 'register', label: 'Register', icon: 'how_to_reg' }, + { key: 'upload', label: 'Upload', icon: 'cloud_upload' }, + { key: 'certify', label: 'Certify', icon: 'verified' }, + { key: 'listing', label: 'Listing', icon: 'add_business' }, + { key: 'complete', label: 'Complete', icon: 'check_circle' }, +]; export default function MyDataPage() { const account = useCurrentAccount(); @@ -40,14 +51,19 @@ export default function MyDataPage() { const [activeTab, setActiveTab] = useState('uploads'); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [uploadStep, setUploadStep] = useState('form'); + const [currentStep, setCurrentStep] = useState('encode'); const [processingType, setProcessingType] = useState<'create' | 'download' | null>(null); const [file, setFile] = useState(null); const [blobId, setBlobId] = useState(''); + const [encryptedObject, setEncryptedObject] = useState(''); const [totalSizeBytes, setTotalSizeBytes] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [uploadError, setUploadError] = useState(''); - const [showTxStatus, setShowTxStatus] = useState(false); + const [listingId, setListingId] = useState(''); + + const [logs, setLogs] = useState([]); const [formData, setFormData] = useState({ name: '', @@ -56,21 +72,22 @@ export default function MyDataPage() { previewSizeBytes: 1024 * 1024, }); - const flowRef = useRef(null); - const iconRef = useRef(null); + const flowRef = useRef> | null>(null); - // Transaction steps for status display - const txSteps = useTransactionSteps([ - { id: 'encode', label: 'Encoding File', description: 'Preparing data for upload' }, - { id: 'register', label: 'Register on Sui', description: 'Sign to register blob' }, - { id: 'upload', label: 'Upload to Walrus', description: 'Uploading encrypted data' }, - { id: 'listing', label: 'Create Listing', description: 'Sign to create marketplace listing' }, - ]); + const addLog = (step: string, status: TransactionLog['status'], message: string, details?: string) => { + setLogs(prev => [...prev, { + step, + status, + message, + details, + timestamp: new Date().toLocaleTimeString(), + }]); + }; - const createFlow = useCallback(async (data: Uint8Array, identifier: string): Promise => { + const createFlow = useCallback(async (data: Uint8Array, identifier: string) => { const walrus = await getWalrus(); const { WalrusFile } = await import('@mysten/walrus'); - + const client = new SuiJsonRpcClient({ url: getFullnodeUrl('testnet'), network: 'testnet', @@ -81,7 +98,7 @@ export default function MyDataPage() { sendTip: { max: 1_000_000 }, }, }) - ) as unknown as { walrus: { writeFilesFlow: (opts: { files: unknown[] }) => WalrusWriteFlow } }; + ); const walrusFile = WalrusFile.from({ contents: data, @@ -99,7 +116,11 @@ export default function MyDataPage() { setFile(selectedFile); setTotalSizeBytes(selectedFile.size); setFormData(prev => ({ ...prev, previewSizeBytes: Math.min(1024 * 1024, selectedFile.size) })); + setUploadStep('form'); setBlobId(''); + setEncryptedObject(''); + setLogs([]); + setCurrentStep('encode'); flowRef.current = null; }; @@ -112,193 +133,189 @@ export default function MyDataPage() { setIsProcessing(true); setUploadError(''); - setShowTxStatus(true); - txSteps.reset(); try { - // Validate form data first const price = parseFloat(formData.priceSUI); if (!formData.name || !formData.description || isNaN(price) || price <= 0) { setUploadError('Please fill in all required fields'); setIsProcessing(false); - setShowTxStatus(false); return; } - // Step 1: Encode - txSteps.startStep('encode', 'Reading and encoding file data...'); - + addLog('encode', 'processing', 'Encoding file...'); + const fileBytes = await file.arrayBuffer(); const uint8Array = new Uint8Array(fileBytes); - // Create flow and encode + // Step 1: Create flow and encode const flow = await createFlow(uint8Array, file.name); flowRef.current = flow; await flow.encode(); + addLog('encode', 'success', 'File encoded'); - // Generate blob ID from content hash - const contentHash = Array.from(uint8Array).slice(0, 32).map((b: number) => b.toString(16).padStart(2, '0')).join(''); - const generatedBlobId = '0x' + contentHash; - const generatedEncryptedObject = '0x' + bytesToHex(uint8Array).slice(0, 128); - - setBlobId(generatedBlobId); - txSteps.completeStep('encode'); - - // Step 2: Register - txSteps.startStep('register', 'Please sign the transaction in your wallet...'); + // Generate encrypted object from file content (first 64 bytes as hex) + const encryptedObj = bytesToHex(uint8Array).slice(0, 128); + setEncryptedObject(encryptedObj); - // Build combined PTB: register + certify + create listing - const { marketplaceConfig, MIST_PER_SUI } = await import('@/config/marketplace'); - const { getMarketplaceTarget } = await import('@/lib/marketplace'); + // Step 2: Register blob on-chain + setCurrentStep('register'); + addLog('register', 'processing', 'Registering blob on Sui...'); - // Get register transaction from flow const registerTx = flow.register({ epochs: 3, owner: account.address, deletable: false, }); - // Execute single PTB transaction signAndExecute( { transaction: registerTx }, { onSuccess: async (result) => { try { - txSteps.completeStep('register'); - - // Step 3: Upload - txSteps.startStep('upload', 'Uploading encrypted data to Walrus network...'); + addLog('register', 'success', 'Blob registered', `TX: ${result.digest.slice(0, 16)}...`); + + // Step 3: Upload to Walrus storage nodes + setCurrentStep('upload'); + addLog('upload', 'processing', 'Uploading to Walrus...'); + await flow.upload({ digest: result.digest }); - txSteps.completeStep('upload'); + addLog('upload', 'success', 'Uploaded to Walrus storage nodes'); - // Step 4: Create Listing - txSteps.startStep('listing', 'Please sign to create your listing...'); + // Step 4: Certify blob + setCurrentStep('certify'); + addLog('certify', 'processing', 'Certifying blob on Sui...'); - // Build final PTB: certify + create listing in one transaction const certifyTx = flow.certify(); - - // Merge certify with create listing using PTB - const priceInMIST = BigInt(Math.floor(price * Number(MIST_PER_SUI))); - const registryId = marketplaceConfig.registryId; - - // Add create listing to the certify transaction - const listing = certifyTx.moveCall({ - target: getMarketplaceTarget('list_dataset'), - arguments: [ - certifyTx.object(registryId), - certifyTx.pure.string(generatedBlobId), - certifyTx.pure.string(generatedEncryptedObject), - certifyTx.pure.string(formData.name), - certifyTx.pure.string(formData.description), - certifyTx.pure.u64(priceInMIST), - certifyTx.pure.u64(formData.previewSizeBytes), - certifyTx.pure.u64(totalSizeBytes), - ], - }); - - // Transfer listing to owner - certifyTx.transferObjects([listing], account.address); - - // Execute combined certify + create listing transaction + signAndExecute( { transaction: certifyTx }, { - onSuccess: () => { - txSteps.completeStep('listing'); - - // Delay to show completion - setTimeout(() => { - setShowTxStatus(false); - setIsCreateModalOpen(false); - setProcessingType('create'); - resetFlow(); - refetch(); - setIsProcessing(false); - }, 1500); + onSuccess: async (certifyResult) => { + addLog('certify', 'success', 'Blob certified', `TX: ${certifyResult.digest.slice(0, 16)}...`); + + // Step 5: Get blobId from listFiles (only available after certify) + let walrusBlobId = ''; + try { + const files = await flow.listFiles(); + console.log('[Walrus] Files:', files); + walrusBlobId = files[0]?.blobId || files[0]?.id || ''; + if (walrusBlobId) { + setBlobId(walrusBlobId); + addLog('complete', 'success', 'Blob ID obtained', `ID: ${walrusBlobId.slice(0, 20)}...`); + } + } catch (listError) { + console.error('[Walrus] listFiles error:', listError); + } + + // Step 6: Create listing + await handleCreateListing(price, walrusBlobId, encryptedObj); }, onError: (err) => { - txSteps.failStep('listing', err.message || 'Failed to create listing'); - setUploadError(err.message || 'Failed to create listing'); + addLog('certify', 'error', err.message || 'Certification failed'); + setUploadError(err.message || 'Certification failed'); setIsProcessing(false); }, } ); } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Upload failed'; - txSteps.failStep('upload', errorMsg); + addLog('upload', 'error', errorMsg); setUploadError(errorMsg); setIsProcessing(false); } }, onError: (err) => { - txSteps.failStep('register', err.message || 'Failed to register blob'); - setUploadError(err.message || 'Failed to register blob'); + addLog('register', 'error', err.message || 'Registration failed'); + setUploadError(err.message || 'Registration failed'); setIsProcessing(false); }, } ); } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Failed to prepare upload'; - txSteps.failStep('encode', errorMsg); + addLog('encode', 'error', errorMsg); + setUploadError(errorMsg); + setIsProcessing(false); + } + }; + + const handleCreateListing = async (price: number, walrusBlobId: string, encryptedObj: string) => { + if (!account) return; + + try { + setCurrentStep('listing'); + addLog('listing', 'processing', 'Creating listing on marketplace...'); + + const priceInMIST = BigInt(Math.floor(price * Number(MIST_PER_SUI))); + const registryId = marketplaceConfig.registryId; + + const tx = new Transaction(); + + const listing = tx.moveCall({ + target: getMarketplaceTarget('list_dataset'), + arguments: [ + tx.object(registryId), + tx.pure.string(walrusBlobId || blobId), + tx.pure.string(encryptedObj), + tx.pure.string(formData.name), + tx.pure.string(formData.description), + tx.pure.u64(priceInMIST), + tx.pure.u64(formData.previewSizeBytes), + tx.pure.u64(totalSizeBytes), + ], + }); + + tx.transferObjects([listing], account.address); + + signAndExecute( + { transaction: tx }, + { + onSuccess: (result) => { + const effects = result.effects as { created?: Array<{ reference: { objectId: string } }> } | undefined; + const newListingId = effects?.created?.[0]?.reference?.objectId || result.digest; + + addLog('listing', 'success', 'Listing created!', `ID: ${newListingId.slice(0, 16)}...`); + setListingId(newListingId); + + setCurrentStep('complete'); + setUploadStep('complete'); + setProcessingType('create'); + setIsProcessing(false); + }, + onError: (err) => { + addLog('listing', 'error', err.message || 'Failed to create listing'); + setUploadError(err.message || 'Failed to create listing'); + setIsProcessing(false); + }, + } + ); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Failed to create listing'; + addLog('listing', 'error', errorMsg); setUploadError(errorMsg); setIsProcessing(false); } }; - const resetFlow = () => { + const closeModal = () => { + setIsCreateModalOpen(false); setFile(null); setBlobId(''); + setEncryptedObject(''); setTotalSizeBytes(0); + setUploadStep('form'); setFormData({ name: '', description: '', priceSUI: '', previewSizeBytes: 1024 * 1024, }); + setLogs([]); + setListingId(''); flowRef.current = null; }; - const closeModal = () => { - setIsCreateModalOpen(false); - resetFlow(); - }; - - useEffect(() => { - if (processingType === 'create') { - const tl = gsap.timeline({ - onComplete: () => { - setTimeout(() => setProcessingType(null), 800); - } - }); - - tl.to('#anim-overlay', { opacity: 1, duration: 0.2 }) - .to('#anim-text', { - duration: 0.5, - onStart: () => { - document.getElementById('anim-text')!.innerText = 'ENCRYPTING METADATA'; - document.getElementById('anim-subtext')!.innerText = 'Preparing asset for encryption'; - } - }) - .to('#anim-progress', { width: '60%', duration: 1.2, ease: 'power2.inOut' }) - .to('#anim-seal', { scale: 1, opacity: 1, rotation: 0, duration: 0.4, ease: 'elastic.out(1, 0.5)' }) - .to('#anim-overlay', { backgroundColor: '#101618', duration: 0.1, yoyo: true, repeat: 1 }, '-=0.2') - .to('#anim-icon', { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' }) - .add(() => { - document.getElementById('anim-text')!.innerText = 'VERIFYING ZK-PROOFS'; - document.getElementById('anim-subtext')!.innerText = 'Generating SNARKs on Sui Network...'; - }) - .to('#anim-progress', { width: '90%', duration: 1 }) - .to('#anim-seal', { borderColor: '#ccff00', boxShadow: '0 0 30px #ccff00', duration: 0.3 }) - .to('#anim-icon', { color: '#ccff00', duration: 0.3 }, '<') - .add(() => { - document.getElementById('anim-text')!.innerText = 'ASSET SECURED'; - document.getElementById('anim-text')!.classList.add('text-accent-lime'); - document.getElementById('anim-subtext')!.innerText = 'Listing created successfully.'; - }) - .to('#anim-progress', { width: '100%', backgroundColor: '#ccff00', duration: 0.3 }) - .to('#anim-seal', { scale: 1.1, duration: 0.2, yoyo: true, repeat: 1 }); - } - }, [processingType]); + const totalRevenue = listings ? listings.reduce((acc, l) => acc + Number(l.price), 0) / 1000000000 : 0; return ( <> @@ -312,7 +329,7 @@ export default function MyDataPage() {

My Data Dashboard

- +
@@ -322,9 +339,7 @@ export default function MyDataPage() {

Total Revenue

-

- {listings ? listings.reduce((acc, l) => acc + Number(l.price), 0) / 1000000000 : 0} -

+

{totalRevenue.toFixed(2)}

SUI
@@ -338,21 +353,23 @@ export default function MyDataPage() {
- - @@ -370,9 +387,9 @@ export default function MyDataPage() { View Analytics arrow_outward
- +
-
setIsCreateModalOpen(true)} className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-ink bg-gray-50 p-6 min-h-[340px] hover:bg-blue-50 hover:border-primary transition-all duration-200 cursor-pointer group" > @@ -425,7 +442,7 @@ export default function MyDataPage() { Recent Purchases (0 items)
- +
@@ -436,7 +453,7 @@ export default function MyDataPage() {

No purchases yet

Start exploring the marketplace to find datasets.

- @@ -448,249 +465,384 @@ export default function MyDataPage() { )}
+
+

Powered by Sui Network

+
+ dataset + security + hub +
+
+ {isCreateModalOpen && (
-
- - -

Create New Listing

+
+
+

Create New Listing

+ +
{!account ? ( -
-
account_balance_wallet
-

Connect Your Wallet

-

Please connect your wallet to create a listing.

+
+
+
account_balance_wallet
+

Connect Your Wallet

+

Please connect your wallet to create a listing.

+
) : ( -
-
-
-
- - handleInputChange('name', e.target.value)} - placeholder="Enter dataset name" - maxLength={100} - className="w-full rounded-lg border-2 border-gray-200 focus:border-primary focus:ring-0 font-bold text-ink placeholder:text-gray-300 transition-colors p-3" - /> -

{formData.name.length}/100 characters

-
- -
- -