Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 107 additions & 17 deletions app/marketplace/my-data/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React, { useState, useRef, useCallback } from 'react';
import Link from 'next/link';
import { useCurrentAccount, useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { useOwnedListings, useAccountBalance } from '@/hooks/useMarketplace';
import { useOwnedListings, useAccountBalance, usePurchasedDatasets } from '@/hooks/useMarketplace';
import { formatSize, bytesToHex, formatPrice } from '@/lib/marketplace';
import { getFullnodeUrl } from '@mysten/sui/client';
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
Expand Down Expand Up @@ -58,6 +58,7 @@ export default function MyDataPage() {
const { mutate: signAndExecute, isPending: isSigning } = useSignAndExecuteTransaction();
const { data: listings, isLoading, refetch } = useOwnedListings(account?.address);
const { data: balance } = useAccountBalance();
const { data: purchases, isLoading: isPurchasesLoading } = usePurchasedDatasets(account?.address);

const [activeTab, setActiveTab] = useState<Tab>('uploads');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
Expand Down Expand Up @@ -598,27 +599,116 @@ export default function MyDataPage() {
<section>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold tracking-tight text-ink flex items-center gap-2">
Recent Purchases <span className="text-gray-400 text-base font-normal ml-1">(0 items)</span>
Recent Purchases <span className="text-gray-400 text-base font-normal ml-1">({purchases?.length || 0} items)</span>
</h2>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<article className="flex flex-col rounded-xl border-2 border-ink bg-white p-4 shadow-hard-sm opacity-60">
<div className="flex items-start justify-between mb-4">
<div className="h-12 w-12 rounded-lg border-2 border-ink bg-gray-200 flex items-center justify-center">
<span className="material-symbols-outlined text-gray-400">shopping_cart</span>
{isPurchasesLoading && (
<>
{[1, 2, 3].map(i => (
<article key={i} className="flex flex-col rounded-xl border-2 border-ink bg-white overflow-hidden animate-pulse">
<div className="h-32 bg-gray-200" />
<div className="p-4 flex flex-col flex-1">
<div className="h-5 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-3 bg-gray-100 rounded w-1/2 mb-3" />
<div className="flex gap-2 mt-auto pt-3 border-t border-gray-200">
<div className="flex-1 h-9 bg-gray-200 rounded-lg" />
</div>
</div>
</article>
))}
</>
)}

{!isPurchasesLoading && purchases && purchases.length > 0 && purchases.map(purchase => (
<article key={purchase.id} className="flex flex-col rounded-xl border-2 border-ink bg-white overflow-hidden">
{/* Header */}
<div className="relative h-32 bg-gradient-to-br from-primary to-blue-600 flex items-center justify-center">
<div className="absolute inset-0 opacity-20">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path d="M0,50 Q25,30 50,50 T100,50" stroke="currentColor" strokeWidth="0.5" fill="none" className="text-white" />
<path d="M0,60 Q25,40 50,60 T100,60" stroke="currentColor" strokeWidth="0.5" fill="none" className="text-white" />
</svg>
</div>
<span className="material-symbols-outlined text-5xl text-white/80">shopping_bag</span>
<span className="absolute top-3 left-3 rounded bg-green-500 text-white px-2 py-0.5 text-[10px] font-bold">PURCHASED</span>
</div>
<span className="rounded bg-gray-100 text-gray-600 px-2 py-1 text-[10px] font-bold border border-gray-200">PENDING</span>
</div>
<h3 className="text-lg font-bold text-gray-400 mb-1">No purchases yet</h3>
<p className="text-sm text-gray-400 mb-4">Start exploring the marketplace to find datasets.</p>
<Link
href="/marketplace"
className="mt-auto w-full h-10 rounded-lg border-2 border-ink bg-gray-100 text-gray-500 font-bold transition-colors flex items-center justify-center gap-2"
>
<span className="material-symbols-outlined text-lg">explore</span> Browse Marketplace
</Link>
</article>

{/* Content */}
<div className="p-4 flex flex-col flex-1">
<h3 className="text-base font-bold text-ink mb-1 line-clamp-2">
{purchase.dataset?.name || 'Dataset'}
</h3>

{/* IDs */}
<p className="text-[10px] text-gray-400 font-mono mb-1 truncate" title={purchase.datasetId}>
Dataset: {purchase.datasetId.slice(0, 8)}...{purchase.datasetId.slice(-6)}
</p>
<p className="text-[10px] text-gray-400 font-mono mb-3 truncate" title={purchase.id}>
Receipt: {purchase.id.slice(0, 8)}...{purchase.id.slice(-6)}
</p>

{/* Stats */}
<div className="grid grid-cols-2 gap-4 py-3 border-t border-gray-200">
<div>
<p className="text-[10px] text-gray-400 uppercase tracking-wide">Paid</p>
<p className="text-sm font-bold text-ink">{formatPrice(purchase.price)}</p>
</div>
<div>
<p className="text-[10px] text-gray-400 uppercase tracking-wide">Size</p>
<p className="text-sm font-bold text-accent-lime">
{purchase.dataset ? formatSize(Number(purchase.dataset.totalSize)) : '-'}
</p>
</div>
</div>

{/* Purchase Date */}
<div className="pt-3 border-t border-gray-200 text-xs text-gray-400">
<span className="material-symbols-outlined text-sm align-middle mr-1">schedule</span>
{new Date(purchase.timestamp).toLocaleDateString()}
</div>

{/* Actions */}
<div className="flex gap-2 mt-auto pt-3 border-t border-gray-200">
<Link
href={`/marketplace/dataset/${purchase.datasetId}`}
className="flex-1 h-9 rounded-lg border-2 border-ink bg-primary text-white text-sm font-bold hover:bg-primary/90 transition-colors flex items-center justify-center gap-1"
>
<span className="material-symbols-outlined text-sm">download</span>
Download
</Link>
<a
href={`https://suiscan.xyz/testnet/object/${purchase.id}`}
target="_blank"
rel="noopener noreferrer"
className="h-9 w-9 rounded-lg border-2 border-ink bg-white text-ink hover:bg-gray-50 transition-colors flex items-center justify-center"
title="View Receipt on SuiScan"
>
<span className="material-symbols-outlined text-sm">open_in_new</span>
</a>
</div>
</div>
</article>
))}

{!isPurchasesLoading && (!purchases || purchases.length === 0) && (
<article className="flex flex-col rounded-xl border-2 border-ink bg-white p-4 shadow-hard-sm opacity-60">
<div className="flex items-start justify-between mb-4">
<div className="h-12 w-12 rounded-lg border-2 border-ink bg-gray-200 flex items-center justify-center">
<span className="material-symbols-outlined text-gray-400">shopping_cart</span>
</div>
</div>
<h3 className="text-lg font-bold text-gray-400 mb-1">No purchases yet</h3>
<p className="text-sm text-gray-400 mb-4">Start exploring the marketplace to find datasets.</p>
<Link
href="/marketplace"
className="mt-auto w-full h-10 rounded-lg border-2 border-ink bg-gray-100 text-gray-500 font-bold transition-colors flex items-center justify-center gap-2"
>
<span className="material-symbols-outlined text-lg">explore</span> Browse Marketplace
</Link>
</article>
)}
</div>
</section>
)}
Expand Down
81 changes: 60 additions & 21 deletions hooks/useMarketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { useCurrentAccount, useSignAndExecuteTransaction, useSuiClient } from '@
import { Transaction } from '@mysten/sui/transactions';
import { useQuery } from '@tanstack/react-query';
import { marketplaceConfig, MIST_PER_SUI } from '@/config/marketplace';
import {
CreateListingInput,
CreateListingResult,
DatasetListing,
PurchaseResult
import {
CreateListingInput,
CreateListingResult,
DatasetListing,
PurchaseResult
} from '@/types/marketplace';
import {
getMarketplaceTarget,
import {
getMarketplaceTarget,
parseDatasetListing
} from '@/lib/marketplace';

Expand Down Expand Up @@ -120,7 +120,7 @@ export function useListDataset() {

// Build PTB with optimized structure
const tx = new Transaction();

// Set gas budget based on operation complexity
tx.setGasBudget(10_000_000); // 0.01 SUI - sufficient for single listing

Expand Down Expand Up @@ -195,7 +195,7 @@ export function useListDataset() {
}

const tx = new Transaction();

// Dynamic gas budget based on number of listings
// Base: 10M MIST + 5M per additional listing
const gasBudget = 10_000_000 + (inputs.length - 1) * 5_000_000;
Expand Down Expand Up @@ -239,12 +239,12 @@ export function useListDataset() {
onSuccess: (result) => {
const effects = result.effects as { created?: Array<{ reference: { objectId: string } }> } | undefined;
const createdIds = effects?.created?.map((obj) => obj.reference.objectId) || [];

const results: CreateListingResult[] = createdIds.map((id) => ({
listingId: id,
digest: result.digest,
}));

onSuccess(results);
},
}
Expand Down Expand Up @@ -369,10 +369,10 @@ export function useOwnedListings(address?: string) {
queryKey: ['owned-listings', address, marketplaceConfig.packageId],
queryFn: async () => {
if (!address) return [];

// Build type string dynamically to ensure env vars are loaded
const listingType = `${marketplaceConfig.packageId}::${marketplaceConfig.moduleName}::DatasetListing`;

const { data } = await suiClient.getOwnedObjects({
owner: address,
filter: { StructType: listingType },
Expand Down Expand Up @@ -414,7 +414,7 @@ export function useListing(listingId: string | undefined) {
queryKey: ['listing', listingId],
queryFn: async () => {
if (!listingId) return null;

const object = await suiClient.getObject({
id: listingId,
options: { showContent: true },
Expand All @@ -441,12 +441,12 @@ export function useAccountBalance() {
queryKey: ['account-balance', account?.address],
queryFn: async () => {
if (!account?.address) return null;

const balance = await suiClient.getBalance({
owner: account.address,
coinType: '0x2::sui::SUI',
});

return {
mist: BigInt(balance.totalBalance),
sui: Number(balance.totalBalance) / Number(MIST_PER_SUI),
Expand All @@ -464,29 +464,68 @@ export function usePurchasedDatasets(address?: string) {
queryKey: ['purchased-datasets', address],
queryFn: async () => {
if (!address) return [];

const RECEIPT_TYPE = `${marketplaceConfig.packageId}::${marketplaceConfig.moduleName}::PurchaseReceipt`;


// Step 1: Get all purchase receipts owned by the user
const { data } = await suiClient.getOwnedObjects({
owner: address,
filter: { StructType: RECEIPT_TYPE },
options: { showContent: true, showType: true },
});

return data
const receipts = data
.map((obj) => {
if (!obj.data?.content || obj.data.content.dataType !== 'moveObject') return null;
const fields = obj.data.content.fields as Record<string, unknown>;
return {
id: obj.data.objectId || '',
datasetId: (fields.dataset_id as { id: string })?.id || '',
datasetId: fields.dataset_id as string,
buyer: fields.buyer as string,
seller: fields.seller as string,
price: BigInt(fields.price as string),
timestamp: Number(fields.timestamp),
};
})
.filter((receipt): receipt is { id: string; datasetId: string; buyer: string; seller: string; price: bigint; timestamp: number } => receipt !== null);

if (receipts.length === 0) return [];

// Step 2: Fetch the dataset details for each purchase receipt
const datasetIds = receipts.map(r => r.datasetId).filter(id => id && id !== '');

if (datasetIds.length === 0) return receipts.map(r => ({ ...r, dataset: null }));

const datasetObjects = await suiClient.multiGetObjects({
ids: datasetIds,
options: { showContent: true },
});

// Create a map of dataset ID -> dataset details
const datasetMap = new Map<string, DatasetListing | null>();
datasetObjects.forEach((obj) => {
if (obj.data?.content?.dataType === 'moveObject') {
const fields = obj.data.content.fields as Record<string, unknown>;
const dataset: DatasetListing = {
id: obj.data.objectId || '',
seller: fields.seller as string,
price: BigInt(fields.price as string),
blobId: fields.blob_id as string,
encryptedObject: fields.encrypted_object as string,
name: fields.name as string,
description: fields.description as string,
previewSize: BigInt(fields.preview_size as string),
totalSize: BigInt(fields.total_size as string),
};
datasetMap.set(obj.data.objectId || '', dataset);
}
});

// Step 3: Combine receipts with dataset details
return receipts.map(receipt => ({
...receipt,
dataset: datasetMap.get(receipt.datasetId) || null,
}));
},
enabled: !!address,
refetchInterval: 10000,
Expand Down Expand Up @@ -583,7 +622,7 @@ export function useDownloadDataset() {
}

const result = await response.json();

if (result.error) {
throw new Error(result.error);
}
Expand Down