This commit is contained in:
Furkan Nabi Sumji 2025-06-04 09:57:17 +05:30
commit 9816e56118
23 changed files with 615 additions and 559 deletions

BIN
Frontend/.DS_Store vendored

Binary file not shown.

View file

@ -117,7 +117,7 @@ export default function Detail() {
shortenAddress(data.user.walletAddress)}
</span>
<span className="text-xs bg-primary/10 w-fit text-primary px-2 py-0.5 rounded">
Owner
Creator
</span>
</div>
</div>

View file

@ -27,6 +27,13 @@ import { RelistForm } from "../shared/RelistForm";
import { RelistModal } from "../shared/RelistModal";
import { client } from "@/lib/suiClient";
import { Clock } from "lucide-react";
import Image from "next/image";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
interface NFTDetailViewProps {
nft: MediaRecord;
@ -83,7 +90,7 @@ export const NFTDetailView = ({
MODULE_NAME,
MARKETPLACE_ID,
nft.blockchain.listingId,
address
address,
);
const stakersCount = await getStakersCount(
@ -92,7 +99,7 @@ export const NFTDetailView = ({
MODULE_NAME,
MARKETPLACE_ID,
nft.blockchain.listingId,
address
address,
);
const bidCount = await getBidCount(
@ -101,7 +108,7 @@ export const NFTDetailView = ({
MODULE_NAME,
MARKETPLACE_ID,
nft.blockchain.listingId,
address
address,
);
const userStake = await getUserStake(
@ -111,7 +118,7 @@ export const NFTDetailView = ({
MARKETPLACE_ID,
nft.blockchain.listingId,
address,
address
address,
);
if (stakersCount) {
@ -125,7 +132,7 @@ export const NFTDetailView = ({
if (userStake) {
setUserStake(userStake);
const result = await convertMistToSuiAndUsd(
Number(userStake.stakeAmount)
Number(userStake.stakeAmount),
);
setConvertedUserStake(result);
}
@ -135,7 +142,6 @@ export const NFTDetailView = ({
setListingDetails(details as unknown as ListingDataResponse);
const result = await convertMistToSuiAndUsd(Number(details.highestBid));
setConverted(result);
//console.log("Listing details:", details);
}
} catch (err) {
console.error("Error fetching listing details:", err);
@ -155,21 +161,12 @@ export const NFTDetailView = ({
try {
setUnstaking(true);
// Log information for debugging
console.log("Unstaking with:", {
address,
listingId: nft.blockchain.listingId,
marketplaceId: MARKETPLACE_ID,
packageId: PACKAGE_ID,
moduleName: MODULE_NAME,
});
// Use the new helper function to build the transaction
// Use the helper function to build the transaction
const transaction = buildWithdrawStakeTx(
MARKETPLACE_ID,
nft.blockchain.listingId,
PACKAGE_ID,
MODULE_NAME
MODULE_NAME,
);
// Execute the transaction
@ -195,15 +192,15 @@ export const NFTDetailView = ({
errorMessage.includes("borrow_child_object_mut")
) {
toast.error(
"Failed to unstake: The listing may not exist or you don't have permission to stake on it."
"Failed to unstake: The listing may not exist or you don't have permission to stake on it.",
);
} else if (errorMessage.includes("Dry run failed")) {
toast.error(
"Failed to unstake: Transaction simulation failed. The listing may not be active."
"Failed to unstake: Transaction simulation failed. The listing may not be active.",
);
} else {
toast.error(
`Failed to unstake: ${errorMessage.substring(0, 100)}...`
`Failed to unstake: ${errorMessage.substring(0, 100)}...`,
);
}
return;
@ -222,10 +219,6 @@ export const NFTDetailView = ({
//console.log(nft);
console.log(listingDetails);
// console.log("stakersCount", stakersCount);
// console.log("bidCount", bidCount);
// console.log("userStake", userStake);
const sold =
!listingDetails?.active &&
@ -249,9 +242,6 @@ export const NFTDetailView = ({
return (
<div className="space-y-8">
<div>
{/* <h1 className="text-2xl font-semibold mb-4 border-b border-stone-300">
{metadata?.name || ""}
</h1> */}
<div className="flex items-center ">
<div className="bg-primary rounded-full p-1" />
<div className=" text-primary px-3 py-1 text-sm">
@ -273,7 +263,6 @@ export const NFTDetailView = ({
) : error ? (
<p className="text-red-500 text-sm">{error}</p>
) : (
<div className="flex flex-col">
<p className="text-2xl font-semibold">
{listingDetails ? `${converted.sui}` : "SUI 0.00"}
@ -282,7 +271,6 @@ export const NFTDetailView = ({
{listingDetails ? `${converted.usd}` : "USD 0.00"}
</p>
</div>
)}
</div>
</div>
@ -307,6 +295,7 @@ export const NFTDetailView = ({
fetchListingDetails={fetchListingDetails}
userStake={userStake}
highestBid={listingDetails?.highestBid}
owner={listingDetails?.owner}
/>
)}
</div>
@ -396,7 +385,7 @@ export const NFTDetailView = ({
<span>SUI</span>
</div>
</div>
{/* <div className="flex justify-between items-center py-2 border-t border-stone-300">
<span className="text-gray-600">Owner</span>
<div className="flex justify-end items-center gap-2">
@ -446,7 +435,10 @@ export const NFTDetailView = ({
target="_blank"
className="hover:underline"
>
<span className="cursor-pointer flex items-center gap-2"> View on Ipfs <svg
<span className="cursor-pointer flex items-center gap-2">
{" "}
View on Ipfs{" "}
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
@ -460,26 +452,38 @@ export const NFTDetailView = ({
/>
</svg>
</span>
</Link>
</div>
)}
<div className="flex justify-between items-center py-2 border-t border-b border-stone-300">
<span className="text-gray-600">Walrus</span>
<span className="text-gray-600">Blob Id</span>
{nft.vector.blobId ? (
<Link
href={`https://walruscan.com/testnet/blob/${nft.vector.blobId}`}
target="_blank"
className="flex items-center gap-2 cursor-pointer"
>
<GiWalrusHead />
<span>{shortenAddress(nft.vector.blobId)}</span>
</Link>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
className="flex items-center gap-2 cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(nft.vector.blobId);
toast.success("Copied BlobId Successfully");
}}
>
<Image
src={"/tusky-wink.svg"}
alt="Tusky Io"
width={17}
height={15}
/>
<span>{shortenAddress(nft.vector.blobId)}</span>
</TooltipTrigger>
<TooltipContent>
<p>Copy to clipboard</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div className="flex items-center gap-2 text-sm cursor-pointer">
<Clock width={15}/>
<Clock width={15} />
Available in 1 hour.
</div>
)}

View file

@ -3,7 +3,7 @@ import { shortenAddress } from "@/utils/shortenAddress";
import { FaRegCopy } from "react-icons/fa";
import { Button } from "../ui/button";
import { toast } from "sonner";
import { useEffect, useState } from "react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { IoMdCloudUpload } from "react-icons/io";
import { AlertCircle, CheckCircle2, Shield, UploadCloud } from "lucide-react";
import { initSocket } from "@/lib/socket";
@ -12,6 +12,7 @@ import { Card } from "../ui/card";
import { AnimatePresence, motion } from "framer-motion";
import { GiWalrusHead } from "react-icons/gi";
import OptimizedImage from "../shared/OptimizedImage";
import { useAuthenticateImage } from "@/hooks/useAuthenticateImage";
interface NFTDetailsProps {
step: number;
@ -58,14 +59,17 @@ const stepProgress = {
"image:softListed": 100,
};
export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps) => {
export const NFTDetails = forwardRef((props: NFTDetailsProps, ref) => {
const { step, compact = false, setStep } = props;
const { sessionId: token } = useImageAuthStore() as AuthState;
const error = useImageAuthStore((s) => s.error);
const [status, setStatus] = useState('')
const [statusStep, setStatusStep] = useState(0)
const [completed, setCompleted] = useState<boolean>(false);
const [requestFailed, setRequestFailed] = useState(false)
const [progress, setProgress] = useState(0);
const [details, setDetails] = useState<
{ label: string; value: string | undefined }[]
>([
@ -79,11 +83,32 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
value: '',
},
{
label: "Vector Url",
label: "Blob Id",
value: '',
},
]);
const resetComponent = () => {
setStatus('');
setStatusStep(0);
setCompleted(false);
setProgress(0);
setImageUrl('');
setDetails([
{ label: "Token Id", value: '' },
{ label: "IPFS URL", value: '' },
{ label: "Status", value: '' },
{ label: "Blob Id", value: '' },
]);
// Also reset Zustand state
useImageAuthStore.getState().setError(null);
useImageAuthStore.getState().setSessionId(null);
useImageAuthStore.getState().reset();
};
useImperativeHandle(ref, () => ({
reset: resetComponent
}));
const [imageUrl, setImageUrl] = useState('')
@ -113,8 +138,8 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
setDetails((prev) => {
// Check if the detail with the label already exists
const newDetail = {
label: "Vector Url",
value: `https://walruscan.com/testnet/blob/${data.blobId}`,
label: "Blob Id",
value: `${data.blobId}`,
};
// Update if exists, otherwise add new
if (prev.some((detail) => detail.label === newDetail.label)) {
@ -127,10 +152,11 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
});
socket.on("image:failed", (data) => {
console.log("Failed: ", data?.message)
setRequestFailed(true)
handleProgress("image:authenticate")
setStatusStep(0)
useImageAuthStore.getState().setError(null)
resetComponent()
setRequestFailed(true)
socket.disconnect();
});
@ -181,7 +207,9 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
return [...prev, newDetail];
});
});
return () => {
socket.disconnect()
}
}, [token])
useEffect(() => {
@ -196,7 +224,7 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
setRequestFailed(false);
}, [step]);
const handleCopy = async (text: string, label: string) => {
if (!text) return;
await navigator.clipboard.writeText(text);
@ -233,6 +261,7 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
</AnimatePresence>
<Button onClick={() => {
setStep(0)
resetComponent()
}}>Try Again</Button>
</Card >
)
@ -310,7 +339,7 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
{[
"Token Id",
"IPFS URL",
"Vector Url",
"Blob Id",
].includes(detail.label)
? shortenAddress(detail.value, 10, 10)
: detail.value}
@ -347,7 +376,7 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
"IPFS URL",
"SHA-256 Hash",
"Perceptual Hash",
"Vector Url",
"Blob Id",
].includes(detail.label)
? shortenAddress(detail.value, 10, 10)
: detail.value}
@ -392,4 +421,4 @@ export const NFTDetails = ({ step, compact = false, setStep }: NFTDetailsProps)
</div>
</>
);
};
});

View file

@ -1,11 +1,13 @@
"use client";
import React from "react";
import React, { useRef, useState } from "react";
import Image from "next/image";
import { IoMdClose } from "react-icons/io";
import { GoDownload } from "react-icons/go";
import { AuthState, useImageAuthStore } from "@/store/useImageAuthStore";
import { NFTDetails } from "./NFTDetails";
type NFTDetailsRef = {
reset: () => void;
};
export function NftMintedDetails({
step,
setStep,
@ -14,14 +16,21 @@ export function NftMintedDetails({
setStep: (step: number) => void;
}) {
const { result } = useImageAuthStore() as AuthState
const [resetKey, setResetKey] = useState(0);
const nftRef = useRef<NFTDetailsRef>(null);
const handleBack = () => {
console.log(nftRef.current?.reset())
nftRef.current?.reset();
setResetKey(prev => prev + 1); // Force re-render
setStep(0); // Or your custom logic to go back
};
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] w-full">
{/* Back button */}
<div className="max-w-4xl w-full">
<button
onClick={() => setStep(0)}
onClick={() => handleBack()}
className="flex items-center gap-1 text-xs text-gray-600 cursor-pointer hover:text-black mb-2 ml-2 mt-4 focus:outline-none"
style={{ alignSelf: "flex-start" }}
>
@ -31,7 +40,7 @@ export function NftMintedDetails({
</div>
{/* NFT Details */}
<NFTDetails step={step} setStep={setStep}/>
<NFTDetails key={resetKey} ref={nftRef} step={step} setStep={setStep}/>

View file

@ -42,7 +42,7 @@ export default function SecureCarousel() {
</div>
))}
</div>
{/* Tailwind custom styles below */}
<style jsx>{`
.animate-marquee {

View file

@ -20,6 +20,12 @@ import { getObjectDetails } from "@/utils/blockchainServices";
import { PACKAGE_ID, MODULE_NAME, MARKETPLACE_ID } from "@/lib/suiConfig";
import { SiSui } from "react-icons/si";
import { client } from "@/lib/suiClient";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
interface NFTCardFeaturedProps {
nft: MediaRecord;
@ -121,11 +127,24 @@ export default function NftAuctionCard({ nft }: NFTCardFeaturedProps) {
/>
{/* Timer overlay */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 w-[160px] h-[40px] rounded-[16px] border border-white/30 bg-white/30 shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur-[5px] flex items-center justify-center">
<span className="text-white text-xs font-semibold mix-blend-difference">
{timeRemaining === "No deadline"
? "loading..."
: timeRemaining}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span className="text-white text-xs font-semibold mix-blend-difference">
{timeRemaining === "No deadline"
? "loading..."
: timeRemaining}
</span>
</TooltipTrigger>
<TooltipContent>
<span className="text-white text-xs font-semibold mix-blend-difference">
{timeRemaining === "No deadline"
? "loading..."
: timeRemaining}
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</Link>
@ -145,11 +164,14 @@ export default function NftAuctionCard({ nft }: NFTCardFeaturedProps) {
<p className="text-xl font-semibold">
{listingDetails && hasHighestBid ? (
<>
{converted.usd}
<span className="text-xs text-gray-400 font-regular ml-1">({converted.sui})</span>
{converted.usd}
<span className="text-xs text-gray-400 font-regular ml-1">
({converted.sui})
</span>
</>
) : "USD 0.00"}
) : (
"USD 0.00"
)}
</p>
</>
)}
@ -186,19 +208,22 @@ export default function NftAuctionCard({ nft }: NFTCardFeaturedProps) {
target="_blank"
className="hover:underline"
>
<span className="cursor-pointer flex items-center gap-2"> View on Ipfs <svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
<span className="cursor-pointer flex items-center gap-2">
{" "}
View on Ipfs{" "}
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</span>
</Link>
</div>
@ -228,7 +253,7 @@ export default function NftAuctionCard({ nft }: NFTCardFeaturedProps) {
{" "}
{nft.user.name || shortenAddress(nft.user.walletAddress) || ""}
</p>
<p className="text-sm text-gray-500">Owner</p>
<p className="text-sm text-gray-500">Creator</p>
</div>
</div>
</div>

View file

@ -1,14 +1,10 @@
import { Button } from "../ui/button";
import { PACKAGE_ID, MODULE_NAME, MARKETPLACE_ID } from "@/lib/suiConfig";
import { useEffect, useState } from "react";
import { useState } from "react";
import {
buildPlaceBidTxWithCoinSelection,
buildPlaceStakeTxWithCoinSelection,
prepareAndBuildBidTransaction,
prepareAndBuildStakeTransaction,
} from "@/utils/blockchainServices";
import { SuiClient } from "@mysten/sui/client";
import { getFullnodeUrl } from "@mysten/sui/client";
import { useWallet } from "@suiet/wallet-kit";
import { toast } from "sonner";
import { MediaRecord } from "@/types";
@ -17,6 +13,7 @@ import { formatSuiAmount } from "@/utils/web2";
interface BIDFormProps {
nft: MediaRecord;
owner?: string;
setOpen?: (open: boolean) => void;
fetchListingDetails?: () => Promise<void>;
userStake?: {
@ -30,14 +27,14 @@ export const BidForm = ({
nft,
setOpen,
fetchListingDetails,
userStake,
owner,
highestBid,
}: BIDFormProps) => {
const [bidAmount, setBidAmount] = useState<string>("");
const [bidding, setBidding] = useState<boolean>(false);
const [staking, setStaking] = useState<boolean>(false);
const [minimumBid, setMinimumBid] = useState<number>(
Number(formatSuiAmount(Number(highestBid))) || 0
Number(formatSuiAmount(Number(highestBid))) || 0,
);
const wallet = useWallet();
@ -67,7 +64,7 @@ export const BidForm = ({
setBidding(true);
const bidAmountMist = BigInt(
Math.floor(parseFloat(bidAmount) * 1_000_000_000)
Math.floor(parseFloat(bidAmount) * 1_000_000_000),
);
const provider = client;
@ -80,7 +77,7 @@ export const BidForm = ({
nft.blockchain.listingId,
bidAmountMist,
PACKAGE_ID,
MODULE_NAME
MODULE_NAME,
);
// If preparation is needed, sign and execute preparation tx
@ -99,7 +96,7 @@ export const BidForm = ({
await new Promise((resolve) => setTimeout(resolve, 3000));
}
// STEP 2: Now build the actual bid transaction (again, to get updated coin state)
// STEP 2: Now build the actual bid transaction
const bidResult = await prepareAndBuildBidTransaction(
provider,
address,
@ -107,11 +104,15 @@ export const BidForm = ({
nft.blockchain.listingId,
bidAmountMist,
PACKAGE_ID,
MODULE_NAME
MODULE_NAME,
);
if (!bidResult.success) {
toast.error(bidResult.message || "Failed to build bid transaction");
toast.error(
bidResult.error ||
bidResult.message ||
"Failed to build bid transaction",
);
return;
}
@ -151,7 +152,7 @@ export const BidForm = ({
setStaking(true);
const stakeAmountMist = BigInt(
Math.floor(parseFloat(bidAmount) * 1_000_000_000)
Math.floor(parseFloat(bidAmount) * 1_000_000_000),
);
const provider = client;
@ -164,7 +165,7 @@ export const BidForm = ({
nft.blockchain.listingId,
stakeAmountMist,
PACKAGE_ID,
MODULE_NAME
MODULE_NAME,
);
// If preparation is needed, sign and execute preparation tx
@ -191,11 +192,15 @@ export const BidForm = ({
nft.blockchain.listingId,
stakeAmountMist,
PACKAGE_ID,
MODULE_NAME
MODULE_NAME,
);
if (!stakeResult.success) {
toast.error(stakeResult.message || "Failed to build stake transaction");
toast.error(
stakeResult.error ||
stakeResult.message ||
"Failed to build stake transaction",
);
return;
}
@ -219,7 +224,6 @@ export const BidForm = ({
}
};
console.log(minimumBid);
return (
<div className="space-y-4">
{/* Bid input */}
@ -230,39 +234,44 @@ export const BidForm = ({
placeholder="0.0"
value={bidAmount}
onChange={handleBidAmountChange}
disabled={bidding || staking}
disabled={bidding || staking || wallet.address == owner}
/>
<span className="text-lg font-medium">SUI</span>
</div>
<div className="w-full flex gap-2">
<Button
className="w-[49%] py-6 text-lg rounded-none"
size="lg"
onClick={handlePlaceBid}
disabled={bidding || staking || !address || !bidAmount}
disabled={
bidding ||
staking ||
!address ||
!bidAmount ||
wallet.address == owner
}
>
{bidding ? "Bidding..." : "Place a Bid"}
</Button>
{/* {userStake && !userStake.hasStaked && ( */}
<Button
className="w-[49%] py-6 text-lg border text-primary rounded-none"
size="lg"
onClick={handlePlaceStake}
variant="outline"
disabled={bidding || staking || !address || !bidAmount}
disabled={
bidding ||
staking ||
!address ||
!bidAmount ||
wallet.address == owner
}
>
{staking ? "Staking..." : "Stake"}
</Button>
{/* )} */}
</div>
{/* {debugInfo && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-xs font-mono overflow-auto max-h-32">
{debugInfo}
</div>
)} */}
</div>
);
};

View file

@ -26,6 +26,7 @@ import { Label } from "@/components/ui/label";
import { BidForm } from "./BidForm";
import { MediaRecord } from "@/types";
import { ListingDataResponse } from "@/types";
import { useWallet } from "@suiet/wallet-kit";
export function ContractForm({
nft,
@ -34,6 +35,7 @@ export function ContractForm({
nft: MediaRecord;
listingDetails: ListingDataResponse | null;
}) {
const wallet = useWallet();
const [open, setOpen] = React.useState(false);
const isDesktop = useMediaQuery({
query: "(min-width: 1224px)",
@ -47,7 +49,10 @@ export function ContractForm({
return (
<Dialog open={open} onOpenChange={setOpen}>
{!sold && (
<DialogTrigger asChild>
<DialogTrigger
asChild
disabled={wallet.address !== listingDetails?.owner}
>
<div className="flex gap-3 mb-8">
<Button className="bg-black text-white px-6 py-2 rounded-none hover:bg-gray-800 transition-colors flex-1">
Place Bid
@ -67,6 +72,7 @@ export function ContractForm({
nft={nft}
setOpen={setOpen}
highestBid={listingDetails?.highestBid}
owner={listingDetails?.owner}
/>
</DialogContent>
</Dialog>
@ -97,6 +103,7 @@ export function ContractForm({
nft={nft}
setOpen={setOpen}
highestBid={listingDetails?.highestBid}
owner={listingDetails?.owner}
/>
</div>
<DrawerFooter className="pt-2">
@ -108,19 +115,3 @@ export function ContractForm({
</Drawer>
);
}
function ProfileForm({ className }: React.ComponentProps<"form">) {
return (
<form className={cn("grid items-start gap-4", className)}>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input type="email" id="email" defaultValue="shadcn@example.com" />
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input id="username" defaultValue="@shadcn" />
</div>
<Button type="submit">Save changes</Button>
</form>
);
}

View file

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -3,11 +3,13 @@ import { useMutation } from "@tanstack/react-query";
import axiosInstance from "@/lib/axios";
import { useImageAuthStore } from "../store/useImageAuthStore";
import { AxiosError } from "axios";
import { useRef } from "react";
export function useAuthenticateImage() {
const setResult = useImageAuthStore((s) => s.setResult);
const setError = useImageAuthStore((s) => s.setError);
const setSessionId = useImageAuthStore((s) => s.setSessionId);
return useMutation({
mutationKey: ["authenticate-image"],
mutationFn: ({ image, name, description }: { image: File, name: string, description: string }) => {
@ -15,7 +17,10 @@ export function useAuthenticateImage() {
formData.append("image", image);
formData.append('metadata', JSON.stringify({ name, description }));
setError('');
// Ensure error state is cleared before sending the request
setError('');
return axiosInstance
.post(`/authenticate`, formData, {
headers: {
@ -28,10 +33,15 @@ export function useAuthenticateImage() {
const axiosErr = error as AxiosError<{ message?: string }>;
const message =
axiosErr.response?.data?.message || axiosErr.message || "Upload failed";
// Check if aborted
if (axiosErr.name === "CanceledError" || message === "canceled") {
console.log("Securing Image Aborted");
}
console.log(error)
throw new Error(message);
})
},
onMutate:() => {
onMutate: () => {
setError('')
},
onSuccess: (data) => {
@ -39,16 +49,17 @@ export function useAuthenticateImage() {
setSessionId(data.sessionId) // Store in Zustand
},
onError: (error: AxiosError) => {
// Try to extract the error message in a robust way
const errorMessage =
(error.response?.data as any)?.message ||
(error.response?.data as any)?.error ||
error.message ||
"Unknown error";
console.log("On Error", errorMessage)
setError(errorMessage);
},
});
}

View file

@ -49,9 +49,9 @@ export function useGetAllImages(
return useQuery<MediaRecord[], Error>({
queryKey: ["nfts", { isActiveOnly, owner }],
queryFn: async () => {
if (isActiveOnly && owner) {
return getActiveNfts(owner);
}
// if (isActiveOnly && owner) {
// return getActiveNfts(owner);
// }
const response = await axiosInstance.get("/all");
return response.data;
},

View file

@ -1,30 +1,28 @@
import { io, Socket } from 'socket.io-client'
let socket: Socket | null = null;
export const initSocket = (token: string): Socket => {
if (!socket) {
socket = io(process.env.NEXT_PUBLIC_BASE_URL, {
transports: ['polling', 'websocket'],
withCredentials: true,
extraHeaders: {
token
}
});
socket.on('connect', () => {
console.log('Socket connected:', socket?.id);
});
const socket = io(process.env.NEXT_PUBLIC_BASE_URL, {
transports: ['polling', 'websocket'],
withCredentials: true,
extraHeaders: {
token
}
});
socket.on('connect', () => {
console.log('Socket connected:', socket?.id);
});
socket.on('connect_error', (err) => {
console.error('Socket connect_error:', err.message);
});
socket.on('disconnect', () => {
console.log('Socket disconnected');
});
}
socket.on('connect_error', (err) => {
console.error('Socket connect_error:', err.message);
});
socket.on('disconnect', () => {
console.log('Socket disconnected');
});
return socket
}
export const getSocket = (): Socket | null => socket;

View file

@ -20,6 +20,7 @@
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.6",
"@suiet/wallet-kit": "^0.3.7",
"@tanstack/react-query": "^5.73.3",
"@tanstack/react-query-devtools": "^5.73.3",

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="153.9184 116.8958 152.022 130.9995" width="152.022px" height="131px" preserveAspectRatio="none"><defs><clipPath id="clip0_4185_254"><rect width="152.022" height="131" fill="white"/></clipPath></defs><g clip-path="url(#clip0_4185_254)" transform="matrix(1, 0, 0, 1, 153.91841125488293, 116.89529418945312)" id="object-0"><path d="M141.912 107.911C146.592 98.5966 151.891 83.8954 151.891 70.3403C151.891 57.3514 148.793 47.3027 142.502 36.6248C141.035 15.5603 127.853 0.000488281 109.106 0.000488281C88.4326 0.000488281 76.4498 18.2698 75.6756 36.3537C55.0171 37.5943 48.6159 57.4069 39.4523 65.6955C25.1067 78.6693 0 78.0843 0 96.8818C0 116.229 25.5707 119.833 58.5373 121.358C59.0933 126.72 69.9603 131.001 83.3238 131.001C95.8551 131.001 106.19 127.236 107.89 122.344C112.444 124.62 119.391 126.077 127.184 126.077C140.903 126.077 152.023 121.569 152.023 116.007C152.023 112.685 148.035 109.746 141.913 107.913L141.912 107.911ZM89.822 82.1151V55.7955C89.822 53.6698 91.5443 51.9462 93.6713 51.9462H97.8749C100.001 51.9462 101.724 53.6698 101.724 55.7955V88.394C101.707 93.6907 101.96 99.7414 104.995 103.085C98.4158 102.063 89.822 96.6953 89.822 82.1151ZM104.851 37.1795H90.1145C89.3845 35.6073 88.9357 33.8056 88.9357 31.8576C88.9357 25.962 92.7623 21.1823 97.4828 21.1823C102.203 21.1823 106.029 25.962 106.029 31.8576C106.029 33.8056 105.582 35.6073 104.851 37.1795ZM115.181 25.986L124.438 21.0927C125.057 20.7674 125.818 21.0032 126.149 21.6198C126.474 22.2376 126.24 23.0054 125.62 23.3307L116.364 28.2239C116.255 28.2807 116.167 28.3576 116.075 28.4307L125.968 27.4271C126.672 27.3527 127.285 27.8646 127.358 28.5593C127.427 29.2553 126.92 29.8756 126.226 29.9475L126.223 29.95L115.825 31.004C116.013 31.2247 116.245 31.4113 116.536 31.5273L125.498 35.1118C126.149 35.3715 126.463 36.1078 126.204 36.7559C126.006 37.2514 125.531 37.5527 125.029 37.5527C124.874 37.5527 124.714 37.5225 124.56 37.4607L115.599 33.8762C114.005 33.2407 112.959 31.7807 112.869 30.0647C112.777 28.3525 113.663 26.7879 115.181 25.986ZM128.293 82.1151C128.293 96.6965 119.701 102.063 113.122 103.085C116.158 99.7427 116.409 93.6907 116.393 88.394V55.7955C116.393 53.6698 118.116 51.9462 120.242 51.9462H124.445C126.571 51.9462 128.295 53.6698 128.295 55.7955L128.293 82.1151Z" style="stroke-width: 3px; stroke: rgb(255, 255, 255);"/></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -6,18 +6,22 @@ import { AxiosError } from "axios";
export type AuthState = {
sessionId: string | null;
result: imageAuthResponse | null;
error: string | null;
error: string | null;
setResult: (data: imageAuthResponse) => void;
setError: (data: string | null) => void;
setSessionId: (data: string | null) => void;
reset: () => void;
};
export const useImageAuthStore = create<AuthState>((set) => ({
sessionId: null,
result: null,
error: null,
setResult: (data) => set({ result: data }),
setError: (data) => set({ error: data }),
setSessionId: (data) => set({ sessionId: data }),
reset: () => set({ sessionId: '', result: null }),
}));

View file

@ -1,14 +1,14 @@
// CONSTANTS
import { Transaction } from "@mysten/sui/transactions";
import { EventId, SuiClient, SuiEvent } from "@mysten/sui/client";
import { SuiClient } from "@mysten/sui/client";
import { MODULE_NAME, PACKAGE_ID } from "@/lib/suiConfig";
const nftTypeArg = `${PACKAGE_ID}::sigillum_nft::PhotoNFT`;
export const buildAcceptBidTx = (
marketplaceObjectId: string,
listingId: string,
packageId: string,
moduleName: string
moduleName: string,
): Transaction => {
const tx = new Transaction();
@ -29,7 +29,7 @@ export const buildWithdrawStakeTx = (
marketplaceObjectId: string,
listingId: string,
packageId: string,
moduleName: string
moduleName: string,
): Transaction => {
const tx = new Transaction();
@ -57,7 +57,7 @@ export async function getObjectDetails(
moduleName: string,
marketplaceObjectId: string,
listingId: string,
address: string | null = null
address: string | null = null,
) {
if (!address) return null;
// If no address is provided, use a default address for read-only operations
@ -76,7 +76,6 @@ export async function getObjectDetails(
sender: senderAddress,
transactionBlock: tx,
});
// console.log("Result:", result);
//Check for dynamic_field error
if (
@ -102,40 +101,40 @@ export async function getObjectDetails(
listPrice: BigInt(
new DataView(Uint8Array.from(returnValues[2][0]).buffer).getBigUint64(
0,
true
)
true,
),
),
listingType: returnValues[3][0][0],
minBid: BigInt(
new DataView(Uint8Array.from(returnValues[4][0]).buffer).getBigUint64(
0,
true
)
true,
),
),
highestBid: BigInt(
new DataView(Uint8Array.from(returnValues[5][0]).buffer).getBigUint64(
0,
true
)
true,
),
),
highestBidder: bytesToHex([...new Uint8Array(returnValues[6][0])]),
active: Boolean(returnValues[7][0][0]),
verificationScore: BigInt(
new DataView(Uint8Array.from(returnValues[8][0]).buffer).getBigUint64(
0,
true
)
true,
),
),
startTime: BigInt(
new DataView(Uint8Array.from(returnValues[9][0]).buffer).getBigUint64(
0,
true
)
true,
),
),
endTime: BigInt(
new DataView(
Uint8Array.from(returnValues[10][0]).buffer
).getBigUint64(0, true)
Uint8Array.from(returnValues[10][0]).buffer,
).getBigUint64(0, true),
),
};
@ -167,7 +166,7 @@ function parseU64FromByteArray(byteArray: number[]): number {
return Number(value);
}
// Otherwise return as number (might lose precision for very large values)
// Otherwise return as number
return Number(value);
}
@ -180,7 +179,7 @@ async function callMoveWithU64Return(
marketplaceObjectId: string,
listingId: string,
address: string | null = null,
errorPrefix: string = "Failed to get value"
errorPrefix: string = "Failed to get value",
): Promise<number | null> {
if (!address) return null;
const tx = new Transaction();
@ -207,7 +206,6 @@ async function callMoveWithU64Return(
// The byte array is the first and only return value
const returnValues = result.results[0].returnValues;
// Based on the example, the u64 value is the first item in returnValues
if (returnValues[0] && returnValues[0][1] === "u64") {
return parseU64FromByteArray(returnValues[0][0]);
}
@ -225,7 +223,7 @@ async function callMoveWithU64Return(
throw new Error(
`${errorPrefix}: ${
error instanceof Error ? error.message : "Unknown error occurred"
}`
}`,
);
}
}
@ -237,7 +235,7 @@ export async function getStakersCount(
moduleName: string,
marketplaceObjectId: string,
listingId: string,
address: string | null = null
address: string | null = null,
) {
return callMoveWithU64Return(
provider,
@ -247,7 +245,7 @@ export async function getStakersCount(
marketplaceObjectId,
listingId,
address,
"Failed to get stakers count"
"Failed to get stakers count",
);
}
@ -258,7 +256,7 @@ export async function getBidCount(
moduleName: string,
marketplaceObjectId: string,
listingId: string,
address: string | null = null
address: string | null = null,
) {
return callMoveWithU64Return(
provider,
@ -268,7 +266,7 @@ export async function getBidCount(
marketplaceObjectId,
listingId,
address,
"Failed to get bid count"
"Failed to get bid count",
);
}
// Function to get fee percentage
@ -277,7 +275,7 @@ export async function getFeePercentage(
packageId: string,
moduleName: string,
marketplaceObjectId: string,
address: string | null = null
address: string | null = null,
) {
if (!address) return null;
const tx = new Transaction();
@ -310,7 +308,7 @@ export async function getTotalVolume(
packageId: string,
moduleName: string,
marketplaceObjectId: string,
address: string | null = null
address: string | null = null,
) {
if (!address) return null;
const tx = new Transaction();
@ -343,7 +341,7 @@ export async function getTotalListings(
packageId: string,
moduleName: string,
marketplaceObjectId: string,
address: string | null = null
address: string | null = null,
) {
if (!address) return null;
const tx = new Transaction();
@ -380,7 +378,7 @@ export async function getListingIds(
limit: number = 10,
onlyActive: boolean = true,
listingType: number = 0,
address: string | null = null
address: string | null = null,
): Promise<{ listingIds: string[]; hasMore: boolean }> {
if (!address) return { listingIds: [], hasMore: false };
@ -434,7 +432,7 @@ export const buildPlaceBidTx = (
packageId: string,
moduleName: string,
bidAmountMist: bigint, // The amount to bid
address: string // User's address
address: string, // User's address
): Transaction => {
const tx = new Transaction();
@ -458,16 +456,13 @@ export const buildPlaceBidTx = (
arguments: [marketplaceArg, listingIdArg, paymentArg],
});
// Return remaining coin to the user
// tx.transferObjects([remainingCoin], tx.pure.address(address));
return tx;
};
export async function buildPrepareCoinsTx(
provider: SuiClient,
address: string,
estimatedGasFee: bigint = BigInt(30_000_000)
estimatedGasFee: bigint = BigInt(30_000_000),
): Promise<{ transaction: Transaction; success: boolean; reason?: string }> {
const { data: coinData } = await provider.getCoins({
owner: address,
@ -483,7 +478,7 @@ export async function buildPrepareCoinsTx(
}
const sorted = [...coinData].sort((a, b) =>
Number(BigInt(b.balance) - BigInt(a.balance))
Number(BigInt(b.balance) - BigInt(a.balance)),
);
const mainCoin = sorted[0];
@ -512,7 +507,7 @@ export async function buildPlaceBidTxWithCoinSelection(
listingId: string,
bidAmountMist: bigint,
packageId: string,
moduleName: string
moduleName: string,
): Promise<{
transaction: Transaction;
success: boolean;
@ -525,23 +520,15 @@ export async function buildPlaceBidTxWithCoinSelection(
coinType: "0x2::sui::SUI",
});
if (!coinData || coinData.length === 0) {
return {
transaction: new Transaction(),
success: false,
error: "No coins available",
};
}
const sortedCoins = [...coinData].sort((a, b) =>
Number(BigInt(b.balance) - BigInt(a.balance))
Number(BigInt(b.balance) - BigInt(a.balance)),
);
const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
const totalBalance = sortedCoins.reduce(
(sum, coin) => sum + BigInt(coin.balance),
BigInt(0)
BigInt(0),
);
if (totalBalance < bidAmountMist + estimatedGasFee) {
@ -554,86 +541,20 @@ export async function buildPlaceBidTxWithCoinSelection(
};
}
let gasCoin = null;
let bidCoin = null;
for (const coin of sortedCoins) {
if (
BigInt(coin.balance) >= estimatedGasFee &&
BigInt(coin.balance) < bidAmountMist + estimatedGasFee
) {
gasCoin = coin;
break;
}
}
if (!gasCoin) {
for (const coin of sortedCoins) {
if (BigInt(coin.balance) >= estimatedGasFee) {
gasCoin = coin;
break;
}
}
}
if (gasCoin) {
for (const coin of sortedCoins) {
if (
coin.coinObjectId !== gasCoin.coinObjectId &&
BigInt(coin.balance) >= bidAmountMist
) {
bidCoin = coin;
break;
}
}
}
if (!gasCoin || !bidCoin) {
return {
transaction: new Transaction(),
success: false,
needsPreparation: true,
error:
"Need to split coin first. Cannot use same coin for gas and bid.",
};
}
const tx = new Transaction();
tx.setSender(address);
tx.setGasBudget(Number(estimatedGasFee));
tx.setGasPayment([
{
objectId: gasCoin.coinObjectId,
version: gasCoin.version,
digest: gasCoin.digest,
},
const splitBidCoins = tx.splitCoins(tx.gas, [
tx.pure.u64(bidAmountMist.toString()),
]);
const bidCoinObj = tx.object(bidCoin.coinObjectId);
if (BigInt(bidCoin.balance) > bidAmountMist) {
const splitBidCoins = tx.splitCoins(bidCoinObj, [
tx.pure.u64(bidAmountMist.toString()),
]);
tx.moveCall({
target: `${packageId}::${moduleName}::place_bid`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
splitBidCoins[0],
],
});
} else {
tx.moveCall({
target: `${packageId}::${moduleName}::place_bid`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
bidCoinObj,
],
});
}
tx.moveCall({
target: `${packageId}::${moduleName}::place_bid`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
splitBidCoins[0],
],
});
return { transaction: tx, success: true };
} catch (error) {
@ -654,12 +575,13 @@ export async function prepareAndBuildBidTransaction(
listingId: string,
bidAmountMist: bigint,
packageId: string,
moduleName: string
moduleName: string,
): Promise<{
transaction: Transaction;
success: boolean;
preparationNeeded?: boolean;
message?: string;
error?: string;
}> {
const result = await buildPlaceBidTxWithCoinSelection(
provider,
@ -668,7 +590,7 @@ export async function prepareAndBuildBidTransaction(
listingId,
bidAmountMist,
packageId,
moduleName
moduleName,
);
if (result.success) return result;
@ -702,7 +624,7 @@ export async function buildPlaceStakeTxWithCoinSelection(
listingId: string,
stakeAmountMist: bigint,
packageId: string,
moduleName: string
moduleName: string,
): Promise<{
transaction: Transaction;
success: boolean;
@ -724,118 +646,40 @@ export async function buildPlaceStakeTxWithCoinSelection(
coinType: "0x2::sui::SUI",
});
if (!coinData || coinData.length === 0) {
return {
transaction: new Transaction(),
success: false,
error: "No SUI coins available.",
};
}
const sortedCoins = [...coinData].sort((a, b) =>
Number(BigInt(b.balance) - BigInt(a.balance))
Number(BigInt(b.balance) - BigInt(a.balance)),
);
const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
const totalBalance = sortedCoins.reduce(
(sum, coin) => sum + BigInt(coin.balance),
BigInt(0)
BigInt(0),
);
if (totalBalance < stakeAmountMist + estimatedGasFee) {
return {
transaction: new Transaction(),
success: false,
error: `Insufficient balance. Need ${
error: `Insufficient balance. You need ${
Number(stakeAmountMist + estimatedGasFee) / 1_000_000_000
} SUI, but have ${Number(totalBalance) / 1_000_000_000} SUI.`,
};
}
let gasCoin = null;
let stakeCoin = null;
// Prepare gasCoin (first step)
for (const coin of sortedCoins) {
if (
BigInt(coin.balance) >= estimatedGasFee &&
BigInt(coin.balance) < stakeAmountMist + estimatedGasFee
) {
gasCoin = coin;
break;
}
}
if (!gasCoin) {
for (const coin of sortedCoins) {
if (BigInt(coin.balance) >= estimatedGasFee) {
gasCoin = coin;
break;
}
}
}
// Prepare stakeCoin (second step)
if (gasCoin) {
for (const coin of sortedCoins) {
if (
coin.coinObjectId !== gasCoin.coinObjectId &&
BigInt(coin.balance) >= stakeAmountMist
) {
stakeCoin = coin;
break;
}
}
}
// If no gas or stake coin available, we need preparation
if (!gasCoin || !stakeCoin) {
return {
transaction: new Transaction(),
success: false,
needsPreparation: true,
error:
"Need to split coins first. Cannot use the same coin for gas and staking.",
} SUI, but you have ${Number(totalBalance) / 1_000_000_000} SUI.`,
};
}
const tx = new Transaction();
tx.setSender(address);
tx.setGasBudget(Number(estimatedGasFee));
tx.setGasPayment([
{
objectId: gasCoin.coinObjectId,
version: gasCoin.version,
digest: gasCoin.digest,
},
const splitStakeCoins = tx.splitCoins(tx.gas, [
tx.pure.u64(stakeAmountMist.toString()),
]);
const stakeCoinObj = tx.object(stakeCoin.coinObjectId);
// Handling coin split if necessary (for stake coin)
if (BigInt(stakeCoin.balance) > stakeAmountMist) {
const splitStakeCoins = tx.splitCoins(stakeCoinObj, [
tx.pure.u64(stakeAmountMist.toString()),
]);
tx.moveCall({
target: `${packageId}::${moduleName}::stake_on_listing`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
splitStakeCoins[0],
],
});
} else {
tx.moveCall({
target: `${packageId}::${moduleName}::stake_on_listing`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
stakeCoinObj,
],
});
}
tx.moveCall({
target: `${packageId}::${moduleName}::stake_on_listing`,
arguments: [
tx.object(marketplaceObjectId),
tx.pure.address(listingId),
splitStakeCoins[0],
],
});
return { transaction: tx, success: true };
} catch (error) {
@ -857,12 +701,13 @@ export async function prepareAndBuildStakeTransaction(
listingId: string,
stakeAmountMist: bigint,
packageId: string,
moduleName: string
moduleName: string,
): Promise<{
transaction: Transaction;
success: boolean;
preparationNeeded?: boolean;
message?: string;
error?: string;
}> {
const result = await buildPlaceStakeTxWithCoinSelection(
provider,
@ -871,7 +716,7 @@ export async function prepareAndBuildStakeTransaction(
listingId,
stakeAmountMist,
packageId,
moduleName
moduleName,
);
if (result.success) return result;
@ -898,158 +743,13 @@ export async function prepareAndBuildStakeTransaction(
return result;
}
// export async function buildPlaceStakeTxWithCoinSelection(
// provider: SuiClient,
// address: string,
// marketplaceObjectId: string,
// listingId: string,
// stakeAmountMist: bigint,
// packageId: string,
// moduleName: string
// ): Promise<{ transaction: Transaction; success: boolean; error?: string }> {
// try {
// console.log("Starting to build stake transaction with params:", {
// address,
// marketplaceObjectId,
// listingId,
// stakeAmountMist: stakeAmountMist.toString(),
// packageId,
// moduleName,
// });
// const { data: coinData } = await provider.getCoins({
// owner: address,
// coinType: "0x2::sui::SUI",
// });
// if (!coinData || coinData.length === 0) {
// return {
// transaction: new Transaction(),
// success: false,
// error: "No SUI coins available.",
// };
// }
// const sortedCoins = [...coinData].sort((a, b) =>
// Number(BigInt(b.balance) - BigInt(a.balance))
// );
// const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
// const totalBalance = sortedCoins.reduce(
// (sum, coin) => sum + BigInt(coin.balance),
// BigInt(0)
// );
// if (totalBalance < stakeAmountMist + estimatedGasFee) {
// return {
// transaction: new Transaction(),
// success: false,
// error: `Insufficient balance. Need ${
// Number(stakeAmountMist + estimatedGasFee) / 1_000_000_000
// } SUI, but have ${Number(totalBalance) / 1_000_000_000} SUI.`,
// };
// }
// let gasCoin = null;
// let stakeCoin = null;
// for (const coin of sortedCoins) {
// if (
// BigInt(coin.balance) >= estimatedGasFee &&
// BigInt(coin.balance) < stakeAmountMist + estimatedGasFee
// ) {
// gasCoin = coin;
// break;
// }
// }
// if (!gasCoin) {
// for (const coin of sortedCoins) {
// if (BigInt(coin.balance) >= estimatedGasFee) {
// gasCoin = coin;
// break;
// }
// }
// }
// if (gasCoin) {
// for (const coin of sortedCoins) {
// if (
// coin.coinObjectId !== gasCoin.coinObjectId &&
// BigInt(coin.balance) >= stakeAmountMist
// ) {
// stakeCoin = coin;
// break;
// }
// }
// }
// if (!gasCoin || !stakeCoin) {
// return {
// transaction: new Transaction(),
// success: false,
// error:
// "Need to split coins first. Cannot use the same coin for gas and staking.",
// };
// }
// const tx = new Transaction();
// tx.setSender(address);
// tx.setGasBudget(Number(estimatedGasFee));
// tx.setGasPayment([
// {
// objectId: gasCoin.coinObjectId,
// version: gasCoin.version,
// digest: gasCoin.digest,
// },
// ]);
// const stakeCoinObj = tx.object(stakeCoin.coinObjectId);
// if (BigInt(stakeCoin.balance) > stakeAmountMist) {
// const splitStakeCoins = tx.splitCoins(stakeCoinObj, [
// tx.pure.u64(stakeAmountMist.toString()),
// ]);
// tx.moveCall({
// target: `${packageId}::${moduleName}::stake_on_listing`,
// arguments: [
// tx.object(marketplaceObjectId),
// tx.pure.address(listingId),
// splitStakeCoins[0],
// ],
// });
// } else {
// tx.moveCall({
// target: `${packageId}::${moduleName}::stake_on_listing`,
// arguments: [
// tx.object(marketplaceObjectId),
// tx.pure.address(listingId),
// stakeCoinObj,
// ],
// });
// }
// return { transaction: tx, success: true };
// } catch (error) {
// console.error("Error building stake transaction:", error);
// return {
// transaction: new Transaction(),
// success: false,
// error: `Failed to build transaction: ${
// error instanceof Error ? error.message : String(error)
// }`,
// };
// }
// }
export async function listNft(
softListingId: string,
listPrice: number,
packageId: string,
moduleName: string,
marketplaceObjectId: string,
nftId: string
nftId: string,
): Promise<{ transaction: Transaction; success: boolean; error?: string }> {
try {
const tx = new Transaction();
@ -1057,9 +757,7 @@ export async function listNft(
const estimatedGasFee = BigInt(50000000); // 0.05 SUI
tx.setGasBudget(Number(estimatedGasFee));
const nftTypeArg =
"0x11fe6fadbdcf82659757c793e7337f8af5198a9f35cbad68a2337d01395eb657::sigillum_nft::PhotoNFT";
tx.moveCall({
target: `${packageId}::${moduleName}::convert_to_real_listing`,
typeArguments: [nftTypeArg],
@ -1090,7 +788,7 @@ export const buildRelistNftTx = async (
newMinBid: number,
newEndTime: number,
packageId: string,
moduleName: string
moduleName: string,
): Promise<Transaction> => {
try {
const tx = new Transaction();
@ -1098,7 +796,6 @@ export const buildRelistNftTx = async (
tx.setGasBudget(Number(estimatedGasFee));
// Use the same NFT type as in your existing code
// This should match the type of your NFT - update if necessary
const nftTypeArg =
"0x11fe6fadbdcf82659757c793e7337f8af5198a9f35cbad68a2337d01395eb657::sigillum_nft::PhotoNFT";
@ -1129,7 +826,7 @@ export async function getUserStake(
marketplaceObjectId: string,
listingId: string,
stakerAddress: string,
callerAddress: string | null = null
callerAddress: string | null = null,
) {
if (!callerAddress) return { hasStaked: false, stakeAmount: 0 };
const tx = new Transaction();
@ -1149,9 +846,6 @@ export async function getUserStake(
transactionBlock: tx,
});
// For debugging (can be removed in production)
// console.log("Raw Result:", JSON.stringify(result, null, 2));
const returnValues = result?.results?.[0]?.returnValues;
if (!returnValues || !Array.isArray(returnValues)) {
return { hasStaked: false, stakeAmount: 0 };
@ -1163,18 +857,17 @@ export async function getUserStake(
// Find the boolean value first
const boolValues = returnValues.filter(
([value, type]) => type === "bool" && Array.isArray(value)
([value, type]) => type === "bool" && Array.isArray(value),
);
if (boolValues.length > 0) {
hasStaked = boolValues[0][0][0] === 1;
}
// Find the stake amount - we need to be smart about this
// There might be multiple u64 values, but we want the one that represents the stake
// Find the stake amount .
const u64Values = returnValues.filter(
([value, type]) =>
type === "u64" && Array.isArray(value) && value.length === 8
type === "u64" && Array.isArray(value) && value.length === 8,
);
if (u64Values.length > 0) {

View file

@ -1316,6 +1316,13 @@
dependencies:
"@radix-ui/react-primitive" "2.1.0"
"@radix-ui/react-arrow@1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz#4b460fdbc1ac097a4964e04ca404c25c2f6d7d3f"
integrity sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==
dependencies:
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/react-collection@1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz"
@ -1440,6 +1447,17 @@
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
"@radix-ui/react-dismissable-layer@1.1.9":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz#46e025ba6e6f403677e22fbb7d99b63cf7b32bca"
integrity sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
"@radix-ui/react-dropdown-menu@^2.1.7":
version "2.1.7"
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.7.tgz"
@ -1571,6 +1589,22 @@
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/rect" "1.1.1"
"@radix-ui/react-popper@1.2.6":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.6.tgz#227d2882f19d80933796525c7bbd0d3ddf699ac0"
integrity sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.6"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-use-rect" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/rect" "1.1.1"
"@radix-ui/react-portal@1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz"
@ -1595,6 +1629,14 @@
"@radix-ui/react-primitive" "2.1.0"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-portal@1.1.8":
version "1.1.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz#0181e85bc0d8c67229dd8cf198204f5f4cc7c09c"
integrity sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==
dependencies:
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-presence@1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz"
@ -1780,6 +1822,24 @@
"@radix-ui/react-roving-focus" "1.1.7"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-tooltip@^1.2.6":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.6.tgz#2311da593951f85d36cd45f4025816bf6feda87e"
integrity sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==
dependencies:
"@radix-ui/primitive" "1.1.2"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dismissable-layer" "1.1.9"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-popper" "1.2.6"
"@radix-ui/react-portal" "1.1.8"
"@radix-ui/react-presence" "1.1.4"
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/react-slot" "1.2.2"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-visually-hidden" "1.2.2"
"@radix-ui/react-use-callback-ref@1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz"
@ -1875,6 +1935,13 @@
dependencies:
"@radix-ui/react-primitive" "2.1.0"
"@radix-ui/react-visually-hidden@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz#aa6d0f95b0cd50f08b02393d25132f52ca7861dc"
integrity sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==
dependencies:
"@radix-ui/react-primitive" "2.1.2"
"@radix-ui/rect@1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz"

View file

@ -46,29 +46,35 @@ export default function BeforeAfterSlide({
}
const handleTouchMove = (e: TouchEvent) => {
if (!containerRef.current) return
if (!isDragging || !containerRef.current) return
e.preventDefault()
const touch = e.touches[0]
const rect = containerRef.current.getBoundingClientRect()
const x = Math.max(0, Math.min(touch.clientX - rect.left, rect.width))
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
const touch = e.touches[0]
const rect = containerRef.current.getBoundingClientRect()
const x = Math.max(0, Math.min(touch.clientX - rect.left, rect.width))
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
setSliderPosition(percentage)
setSliderPosition(percentage)
}
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp)
window.addEventListener("touchmove", handleTouchMove, { passive: false })
window.addEventListener("touchend", handleMouseUp)
if (!isDragging) return
return () => {
window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", handleMouseUp)
window.removeEventListener("touchmove", handleTouchMove)
window.removeEventListener("touchend", handleMouseUp)
}
const handleTouchMoveWithPrevent = (e: TouchEvent) => {
e.preventDefault() // allowed only because passive is false
handleTouchMove(e)
}
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp)
window.addEventListener("touchmove", handleTouchMoveWithPrevent, { passive: false })
window.addEventListener("touchend", handleMouseUp)
return () => {
window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", handleMouseUp)
window.removeEventListener("touchmove", handleTouchMoveWithPrevent)
window.removeEventListener("touchend", handleMouseUp)
}
}, [isDragging])
return (

View file

@ -299,8 +299,8 @@ const Verification = ({ image, verificationError, verificationData, isVerifying,
</div>
<div className="flex gap-4 pt-4 border-t border-[#f1f3f5]">
<Link href={`https://sigillum.digital/detail/${verificationData.verifications[0]._id}`} target='_blank' className='w-[50%] flex'>
<Button className="md:flex-1 px-3 bg-[#000] hover:bg-gray-950 rounded-none text-white gap-2 md:py-6 py-3 border border-black">
<Link href={`https://sigillum.digital/detail/${verificationData.verifications[0]._id}`} target='_blank' className='w-[50%] flex-1'>
<Button className="md:flex-1 w-full px-3 bg-[#000] hover:bg-gray-950 rounded-none text-white gap-2 md:py-6 py-3 border border-black">
<ExternalLink className="w-4 h-4" />
View Lisitng
</Button>

View file

@ -33,7 +33,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" className="light" style={{ colorScheme: 'light' }}>
<body className={`${albertSans.className} bg-white`}>
<body className={`${albertSans.className} bg-white overflow-x-hidden`}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
{children}
<Toaster />

148
README.md
View file

@ -1 +1,147 @@
# Sigillum
# Sigillum: The Image Authentication Protocol
![image](https://github.com/user-attachments/assets/975ab6ac-7196-4e8b-8a05-c636d1a76acc)
**Empowering creators for their creations**
Sigillum is a revolutionary platform that combines advanced image verification technology with blockchain to authenticate digital imagery and create a trusted marketplace for genuine digital moments.
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/sigillumpro.svg?style=social&label=Follow%20%40SigillumPro)](https://twitter.com/sigillumpro)
## Overview
In an era where digital manipulation is commonplace, Sigillum offers a solution to verify, certify, and trade authentic digital images. Our platform provides creators with the means to prove their work's authenticity and collectors the confidence to invest in verified digital assets.
![image](https://github.com/user-attachments/assets/84e101d0-38d9-4b70-acfd-1c8f0bad2736)
## Verifier App
Url: https://verifier.sigillum.digital/
## Key Features
### Image Authentication
- **Proprietary Verification Technology**: Advanced algorithms detect manipulation and verify image authenticity
- **OpenAI CLIP Integration**: Leverages state-of-the-art AI for deep image analysis and recognition
- **Authenticity Scoring**: Each image receives a Sigillum Score indicating confidence level in its authenticity
- **Split-Screen Verification**: Visual comparison between original and verified versions
- **Creator Attribution**: Immutable record of image creator and creation date
- **Plagiarism Detection**: Identifies copied or derivative content
### Blockchain Certification
- **SUI Blockchain Integration**: Images are minted on the SUI blockchain for permanent verification
- **Decentralized Storage**: Image metadata stored on IPFS/Walrus for true decentralization
- **Unique Token IDs**: Each authenticated image receives a unique blockchain identifier
- **Provenance History**: Complete, immutable record of ownership and transactions
### NFT Marketplace
- **Live Auctions**: Bid on verified images with transparent auction mechanics
- **Staking System**: Stake tokens on image authenticity for additional verification
- **Direct Sales**: Buy and sell authenticated images at fixed prices
- **Featured Drops**: Curated collections of notable authentic imagery
- **Dynamic Pricing**: Value increases based on real-world virality and engagement
### Web2 Native Verification
- **Mobile Verifier App**: Scan any image in the wild with your smartphone
- **Social Media Integration**: Verify images across social platforms instantly
- **Universal Accessibility**: No blockchain knowledge required for verification
- **Bridge to Web3**: Seamless onboarding to blockchain ecosystem
## Real-World Value Dynamics
Sigillum introduces a groundbreaking feature that links real-world engagement to marketplace value:
- **Virality Tracking**: More viral or valuable an image becomes in the real world, the more its value increases in the Sigillum marketplace
- **Free Market Principles**: Natural price discovery based on actual usage and cultural impact
- **Creator Rewards**: Original creators benefit from their work's success long after initial sale
- **Value Recognition**: The system rewards truly impactful content rather than artificial scarcity
## Architecture
![image](https://github.com/user-attachments/assets/577da540-348e-44f0-afcf-d7c8aaf609ca)
Sigillum operates on a dual-system architecture:
### Web3 Integrated Side
- Authentication pipeline powered by OpenAI CLIP for image analysis
- Storage infrastructure using Pinata, Qdrant, and Walrus
- Minting process on SUI blockchain
- Marketplace operations with smart contracts
### Web2 Native Side
- Mobile verification app accessible to everyone
- Scans images from any source (screens, prints, public displays)
- Provides immediate ownership and provenance information
- Creates an entry point to the Web3 ecosystem
![image](https://github.com/user-attachments/assets/eb1f27f5-2cb7-4776-b474-900a112f8a39)
## DeFi Flow
Our platform features a comprehensive financial flow:
1. Creator uploads and authenticates an image
2. Image enters the Sigillum Authentication Pipeline
3. Authenticated image is minted as an NFT
4. Creator can list the NFT on the marketplace
5. Users can bid on or stake in the listing
6. Ownership transfers upon sale completion
7. Stakers receive a share of the sale price
8. Viral engagement increases market value
## Getting Started
### For Creators
1. **Create an Account**: Sign up at [sigillum.digital](https://sigillum.digital)
2. **Upload Your Image**: Submit your authentic digital image for verification
3. **Verification Process**: Our system analyzes your image and provides an authenticity score
4. **Mint Your NFT**: Once verified, mint your image as an NFT on the SUI blockchain
5. **List for Sale**: Choose to list your authenticated NFT in our marketplace
### For Collectors
1. **Browse the Marketplace**: Explore verified authentic images
2. **Check Verification Details**: Review authenticity scores and verification data
3. **Place Bids or Buy**: Participate in auctions or purchase images directly
4. **Verify Ownership**: All transactions are recorded on the blockchain for transparency
### For General Users
1. **Download the App**: Get the Sigillum Verifier app from your app store
2. **Scan Any Image**: Point your camera at an image on any medium
3. **Instant Information**: Get immediate details about ownership and authenticity
4. **Explore Further**: Follow links to view the image on the marketplace
## Technology Stack
- **Frontend**: NextJs
- **Backend**: Node.js, Express, Move
- **Blockchain**: SUI Network
- **Storage**: IPFS/Walrus
- **AI Analysis**: OpenAI CLIP
- **Vector Database**: Qdrant
- **Vector Storage**: Walrus for AI data sets and images
- **Authentication**: Proprietary image verification algorithms
## API Documentation
To be released soon...
## Smart Contracts
Our smart contracts are open source and available for review:
- [SigillumNFT Contract](https://github.com/sigillum/contracts/SigillumNFT.move)
- [SigillumMarketplace Contract](https://github.com/sigillum/contracts/SigillumMarketplace.move)
## Contact
- Website: [sigillum.digital](https://sigillum.digital)
- Email: SigillumProtocol@proton.me
- Twitter: [@SigillumPro](https://x.com/sigillumpro)
---
© 2025 Sigillum. All rights reserved.

View file

@ -229,7 +229,7 @@ export const uploadImage = async (req: FileRequest, res: Response): Promise<void
if(!blobId) {
notifyFailed(sessionId, 'Failed to add blob');
return;
}
}
// Send notification - Blob uploaded
notifyBlobUploaded(sessionId, blobId);
// Upload original image to IPFS