diff --git a/README.md b/README.md index d880f97..dd722a7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ This Turborepo includes the following example applications: | `@shelby-protocol/cross-chain-accounts` | Shelby cross chain accounts example | [`apps/cross-chain-accounts`](./apps/cross-chain-accounts) | | `@shelby-protocol/download-example` | An example app to demonstrate downloading blobs using the Shelby SDK | [`apps/download-blob`](./apps/download-blob) | | `@shelby-protocol/list-example` | An example app to demonstrate listing blobs using the Shelby SDK | [`apps/list-blob`](./apps/list-blob) | +| `@shelby-protocol/streaming-video` | Shelby Streaming Video example with Gating | [`apps/streaming-video`](./apps/streaming-video) | | `@shelby-protocol/upload-example` | An example app to demonstrate uploading blobs using the Shelby SDK | [`apps/upload-blob`](./apps/upload-blob) | diff --git a/apps/solana/token-gated/package.json b/apps/solana/token-gated/package.json index 1c76114..0e3e582 100644 --- a/apps/solana/token-gated/package.json +++ b/apps/solana/token-gated/package.json @@ -26,6 +26,7 @@ "version": "0.0.0", "private": true, "dependencies": { + "@aptos-labs/ace-sdk": "^0.1.0", "@coral-xyz/anchor": "^0.31.1", "@shelby-protocol/react": "latest", "@shelby-protocol/sdk": "latest", @@ -35,7 +36,6 @@ "@solana/react-hooks": "^1.1.5", "@solana/web3.js": "^1.98.4", "@tanstack/react-query": "^5.90.16", - "@aptos-labs/ace-sdk": "^0.1.0", "next": "16.0.10", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -55,4 +55,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/apps/streaming-video/.env.example b/apps/streaming-video/.env.example new file mode 100644 index 0000000..926e2de --- /dev/null +++ b/apps/streaming-video/.env.example @@ -0,0 +1,5 @@ +# Shelby Protocol Configuration +NEXT_PUBLIC_SHELBY_API_URL=https://api.shelbynet.shelby.xyz + +# Shelby API Key for enhanced rate limits +NEXT_PUBLIC_SHELBY_API_KEY=your_shelby_api_key_here diff --git a/apps/streaming-video/README.md b/apps/streaming-video/README.md new file mode 100644 index 0000000..2f89f89 --- /dev/null +++ b/apps/streaming-video/README.md @@ -0,0 +1,58 @@ +# Shelby Streaming Video Example + +This example demonstrates how to build a video streaming application with decentralized content gating using the Shelby Protocol. + +## Key Features + +- **Real-time Streaming**: Utilizes Shelby SDK's `streamData` for low-latency video playback. +- **On-chain Gating**: Protects premium content with rules stored on-chain (Aptos/Solana). +- **Micropayments**: Integrated "Tip-to-Unlock" flow for content monetization. +- **Wallet Integration**: Seamless connection with Aptos wallets (Petra, etc.). +... +2. **Access Rules**: Creators set a price (e.g., 0.5 APT) and their wallet address receives payments directly. + +## Getting Started + +### 1. Installation + +Install dependencies from the root of the monorepo: + +```bash +pnpm install +``` + +### 2. Environment Setup + +Copy `.env.example` to `.env` and fill in your API keys: + +```bash +cp .env.example .env +``` + +Required variables: +- `NEXT_PUBLIC_SHELBY_API_KEY`: Your Shelby Protocol API key. +- `NEXT_PUBLIC_APTOS_API_KEY`: Aptos network API key. + +### 3. Development + +Run the development server: + +```bash +pnpm dev --filter=@shelby-protocol/streaming-video +``` + +Open [http://localhost:3005](http://localhost:3005) to view the app. + +## How it Works + +1. **Content Upload**: Videos are encrypted and uploaded as blobs to Shelby storage. +2. **Access Rules**: A access policy is set (e.g., "Must pay 0.1 APT to address X"). +3. **Streaming**: When a user visits the page, the app checks for the payment. If not found, the `VideoPlayer` displays a lock screen. +4. **Unlocking**: Upon payment, the Shelby SDK provides the authorized stream to the browser. + +## Built With + +- [Next.js](https://nextjs.org/) +- [Shelby SDK](https://docs.shelby.xyz) +- [React Player](https://github.com/cookpete/react-player) +- [Tailwind CSS](https://tailwindcss.com/) diff --git a/apps/streaming-video/app/favicon.ico b/apps/streaming-video/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/apps/streaming-video/app/favicon.ico differ diff --git a/apps/streaming-video/app/fonts/GeistMonoVF.woff b/apps/streaming-video/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/apps/streaming-video/app/fonts/GeistMonoVF.woff differ diff --git a/apps/streaming-video/app/fonts/GeistVF.woff b/apps/streaming-video/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/apps/streaming-video/app/fonts/GeistVF.woff differ diff --git a/apps/streaming-video/app/globals.css b/apps/streaming-video/app/globals.css new file mode 100644 index 0000000..200400d --- /dev/null +++ b/apps/streaming-video/app/globals.css @@ -0,0 +1 @@ +@import "@shelby-protocol/ui/globals.css"; diff --git a/apps/streaming-video/app/layout.tsx b/apps/streaming-video/app/layout.tsx new file mode 100644 index 0000000..71ecf81 --- /dev/null +++ b/apps/streaming-video/app/layout.tsx @@ -0,0 +1,36 @@ +import { Toaster } from "@shelby-protocol/ui/components"; +import localFont from "next/font/local"; +import { WalletProvider } from "@/components/WalletProvider"; +import "./globals.css"; +import type { Metadata } from "next"; + +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", +}); + +export const metadata: Metadata = { + title: "Shelby Streaming Video Example", + description: "A decentralized video streaming example with content gating using Shelby Protocol", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + + ); +} diff --git a/apps/streaming-video/app/page.tsx b/apps/streaming-video/app/page.tsx new file mode 100644 index 0000000..a36379a --- /dev/null +++ b/apps/streaming-video/app/page.tsx @@ -0,0 +1,354 @@ +"use client"; + +import { useState, useRef } from "react"; +import { Header } from "@/components/Header"; +import { VideoPlayer } from "@/components/VideoPlayer"; +import { VideoUploader } from "@/components/VideoUploader"; +import { LoadingSpinner } from "@/components/Loading"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { toast } from "sonner"; +import { PlayCircle, Upload, ListVideo, User, Trash2, ChevronLeft, ChevronRight } from "lucide-react"; +import { useVideoStorage, VideoMetadata } from "@/hooks/useVideoStorage"; + +export default function Home() { + const { signAndSubmitTransaction, account } = useWallet(); + const scrollContainerRef = useRef(null); + + // Navigation State + const [mainTab, setMainTab] = useState<"watch" | "upload">("watch"); + const [filterTab, setFilterTab] = useState<"all" | "my">("all"); + + // App State + const [isLocked, setIsLocked] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [loadingText, setLoadingText] = useState(""); + const [activeVideo, setActiveVideo] = useState(null); + + // Refresh Mechanism + const [refreshKey, setRefreshKey] = useState(0); + const { videos, removeVideo } = useVideoStorage(refreshKey); + + const handleRefresh = () => { + setRefreshKey(prev => prev + 1); + }; + + const filteredVideos = filterTab === 'all' + ? videos + : videos.filter(v => account?.address && v.owner === account.address.toString()); + + // Carousel Logic + const scroll = (direction: 'left' | 'right') => { + if (scrollContainerRef.current) { + const container = scrollContainerRef.current; + const scrollAmount = container.clientWidth * 0.8; // Scroll 80% screen width + container.scrollBy({ + left: direction === 'right' ? scrollAmount : -scrollAmount, + behavior: 'smooth' + }); + } + }; + + const getPlayUrl = (video: VideoMetadata) => { + // If video has a direct URL, use it + if (video.url) { + return video.url; + } + // If blobName is already a full URL, use it directly + if (video.blobName.startsWith('http')) { + return video.blobName; + } + // Otherwise, construct Shelby API URL + const owner = video.owner || "0x1"; + return `https://api.shelbynet.shelby.xyz/shelby/v1/blobs/${owner}/${video.blobName}`; + }; + + const getVideoTitle = (blobName: string) => { + // Remove timestamp suffix (last segment after final hyphen) and format + const nameWithoutTimestamp = blobName.replace(/-\d+$/, ''); + return nameWithoutTimestamp.replace(/_/g, ' '); + }; + + const handleUnlock = async () => { + if (!account) { + toast.error("Please connect your wallet first!"); + return; + } + + // Allow owners to view their own content without payment + if (activeVideo?.owner === account.address.toString()) { + setIsLocked(false); + toast.success("Content unlocked! (You own this video)"); + return; + } + + if (!activeVideo?.owner) { + toast.error("Video owner information is missing. Cannot process payment."); + return; + } + + try { + setIsLoading(true); + setLoadingText("Processing Payment on Aptos..."); + + // Use string-based calculation to avoid floating point precision errors + const priceFloat = parseFloat(activeVideo.price || "0"); + + // Allow free view if price is exactly 0 (for demo videos) + if (priceFloat === 0) { + setIsLocked(false); + toast.success("Demo content unlocked for free!"); + return; + } + + if (isNaN(priceFloat) || priceFloat < 0) { + toast.error("Invalid video price."); + setIsLoading(false); + return; + } + const amountInOctas = Math.round(priceFloat * 100000000); + + // Ensure the amount is at least 1 octa to prevent free unlocks + if (amountInOctas < 1) { + toast.error("Price too small. Minimum is 0.00000001 APT."); + setIsLoading(false); + return; + } + + const transactionResponse = await signAndSubmitTransaction({ + data: { + function: "0x1::coin::transfer", + typeArguments: ["0x1::aptos_coin::AptosCoin"], + functionArguments: [activeVideo.owner, amountInOctas], + }, + }); + + console.log("Transaction Hash:", transactionResponse.hash); + + // NOTE: This is a demo implementation. In production, you should: + // 1. Wait for transaction confirmation on-chain + // 2. Verify the payment was successful and went to the correct address + // 3. Only unlock content after verification + // Current implementation unlocks immediately after transaction submission + + setIsLocked(false); + toast.success("Content Unlocked! Transaction submitted: " + transactionResponse.hash.slice(0, 10) + "..."); + + } catch (error: any) { + console.error(error); + toast.error(error.message || "Payment failed. Please try again."); + } finally { + setIsLoading(false); + setLoadingText(""); // Clear loading text to prevent stale state + } + }; + + const handleSelectVideo = (video: VideoMetadata) => { + setActiveVideo(video); + setIsLocked(true); + setLoadingText(""); // Clear any stale loading text + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+
+ {isLoading && } + +
+ + {/* Main Navigation Tabs */} +
+ + +
+ + {/* Content Area */} + {mainTab === "watch" ? ( +
+ + {activeVideo ? ( + /* Player Mode */ +
+ +
+ +
+
+ ) : ( + /* Gallery Mode - Carousel Layout */ + <> + {/* Sub-Filters for Gallery */} +
+ + +
+ + {/* Carousel Container */} +
+ + {/* Left Arrow */} + {filteredVideos.length > 5 && ( + + )} + + {/* Scrollable Row */} +
+ {filteredVideos.map((vid) => { + const isOwner = account?.address && vid.owner === account.address.toString(); + + return ( +
handleSelectVideo(vid)} + className=" + shrink-0 + w-full sm:w-[calc(50%-8px)] md:w-[calc(33.33%-11px)] lg:w-[calc(25%-12px)] xl:w-[calc(20%-13px)] + group aspect-square bg-black border border-white/10 hover:border-blue-500/50 rounded-2xl overflow-hidden cursor-pointer flex flex-col transition-all duration-300 hover:shadow-2xl hover:shadow-blue-900/10 hover:-translate-y-1 + " + > + {/* Top 20%: Actions & Price */} +
+
+ {isOwner && ( + + )} +
+
+ {vid.price} APT +
+
+ + {/* Middle 60%: Video Thumbnail */} +
+
+ + {/* Bottom 20%: Info */} +
+

+ {getVideoTitle(vid.blobName)} +

+
+ + + {vid.owner ? `${vid.owner.slice(0, 4)}...${vid.owner.slice(-4)}` : 'Unknown'} + +
+
+
+ ); + })} +
+ + {/* Right Arrow */} + {filteredVideos.length > 5 && ( + + )} +
+ + {filteredVideos.length === 0 && ( +
+

No videos found here

+ {filterTab === 'my' ? ( +

You haven't uploaded any videos yet.

+ ) : ( +

Be the first to upload content!

+ )} +
+ )} + + )} +
+ ) : ( +
+ +
+ )} + +
+
+ ); +} diff --git a/apps/streaming-video/components.json b/apps/streaming-video/components.json new file mode 100644 index 0000000..e1847ad --- /dev/null +++ b/apps/streaming-video/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "../../packages/ui/src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "hooks": "@/hooks", + "lib": "@/lib", + "utils": "@shelby-protocol/ui/lib/utils", + "ui": "@shelby-protocol/ui/components" + } +} diff --git a/apps/streaming-video/components/Header.tsx b/apps/streaming-video/components/Header.tsx new file mode 100644 index 0000000..8be04e4 --- /dev/null +++ b/apps/streaming-video/components/Header.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { Button } from "@shelby-protocol/ui/components/button"; +import { useState, useRef, useEffect } from "react"; +import { ChevronDown, LogOut, Copy } from "lucide-react"; +import { toast } from "sonner"; + +export const Header = () => { + const { connected, account, connect, wallets, disconnect } = useWallet(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const onFaucetAptos = () => { + if (!account) return; + window.open( + `https://docs.shelby.xyz/apis/faucet/aptos?address=${account.address}`, + "_blank" + ); + }; + + const onMintShelbyUsd = () => { + if (!account) return; + window.open( + `https://docs.shelby.xyz/apis/faucet/shelbyusd?address=${account.address}`, + "_blank", + ); + }; + + const copyAddress = () => { + if (account?.address) { + navigator.clipboard.writeText(account.address.toString()); + toast.success("Address copied!"); + setIsDropdownOpen(false); + } + }; + + return ( +
+
+
+

+ Shelby Stream +

+
+
+ {/* Faucet Buttons only visible when connected */} + {connected && ( + <> + + + + )} + + {/* Custom Wallet Connection UI with Dropdown */} + {connected && account ? ( +
+ + + {isDropdownOpen && ( +
+
+ +
+ +
+
+ )} +
+ ) : ( +
+ {wallets.length > 0 && (() => { + const preferredWallet = wallets.find(w => w.name.toLowerCase().includes('petra')) || wallets[0]; + return ( + + ); + })()} + {wallets.length === 0 && ( + + )} +
+ )} +
+
+
+ ); +}; diff --git a/apps/streaming-video/components/Loading.tsx b/apps/streaming-video/components/Loading.tsx new file mode 100644 index 0000000..1234981 --- /dev/null +++ b/apps/streaming-video/components/Loading.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { Loader2 } from "lucide-react"; + +export function LoadingSpinner({ text = "Processing..." }: { text?: string }) { + return ( +
+ +

{text}

+
+ ); +} diff --git a/apps/streaming-video/components/VideoPlayer.tsx b/apps/streaming-video/components/VideoPlayer.tsx new file mode 100644 index 0000000..f38eca9 --- /dev/null +++ b/apps/streaming-video/components/VideoPlayer.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; + +// Dynamically import ReactPlayer to avoid SSR hydration issues +const ReactPlayer = dynamic(() => import('react-player'), { ssr: false }); + +interface VideoPlayerProps { + url: string; + isLocked: boolean; + onUnlock: () => void; + poster?: string; + loadingText?: string; + title?: string; + description?: string; + price?: string; +} + +export const VideoPlayer = ({ url, isLocked, onUnlock, poster, loadingText, title, description, price }: VideoPlayerProps) => { + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + // Auto-play logic when unlocked + if (!isLocked) { + setIsPlaying(true); + } else { + setIsPlaying(false); + } + }, [isLocked]); + + return ( +
+ {isLocked && ( +
+ {loadingText ? ( +
+
+

{loadingText}

+
+ ) : ( +
+

{title || "Premium Content"}

+ {description &&

{description}

} + +
+

Unlock Price

+

{price || "0.1"} APT

+
+ + +
+ )} +
+ )} + +
+ console.log("VideoPlayer: Ready to play", url)} + onStart={() => console.log("VideoPlayer: Started playing")} + onError={(e) => console.error("VideoPlayer: Error playing video", e, url)} + playIcon={ +
+ + + +
+ } + config={{ + file: { + attributes: { + controlsList: 'nodownload', // Disable download button + crossOrigin: 'anonymous' + }, + forceVideo: true // Force usage of video tag if extension is missing + } + }} + /> +
+
+ ); +}; diff --git a/apps/streaming-video/components/VideoUploader.tsx b/apps/streaming-video/components/VideoUploader.tsx new file mode 100644 index 0000000..569f3ac --- /dev/null +++ b/apps/streaming-video/components/VideoUploader.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState, useRef } from "react"; +import { Button } from "@shelby-protocol/ui/components/button"; +import { Input } from "@shelby-protocol/ui/components/input"; +import { Check, Loader2, UploadCloud } from "lucide-react"; +import { useUploadVideo } from "@/hooks/useUploadVideo"; +import { useVideoStorage } from "@/hooks/useVideoStorage"; +import { toast } from "sonner"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; + +export const VideoUploader = ({ onSuccess }: { onSuccess?: () => void }) => { + const [file, setFile] = useState(null); + const [price, setPrice] = useState("0.1"); + const [description, setDescription] = useState(""); + const [uploadedBlobName, setUploadedBlobName] = useState(null); + const fileInputRef = useRef(null); + + const { uploadVideo, isUploading } = useUploadVideo(); + const { addVideo } = useVideoStorage(); + const { account } = useWallet(); + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setFile(e.target.files[0]); + setUploadedBlobName(null); + } + }; + + const handleUpload = async () => { + if (!file) { + toast.error("Please select a video file first."); + return; + } + + // Capture values now to avoid race conditions if user edits inputs during upload + const capturedPrice = price; + const capturedDescription = description || "Uploaded Video"; + + // Validate price + const priceFloat = parseFloat(capturedPrice); + if (isNaN(priceFloat) || priceFloat <= 0) { + toast.error("Please enter a valid price greater than 0."); + return; + } + + // Check for minimum octa amount (matching page.tsx) + const amountInOctas = Math.round(priceFloat * 100000000); + if (amountInOctas < 1) { + toast.error("Price too small. Minimum is 0.00000001 APT."); + return; + } + + try { + const result = await uploadVideo(file); + if (result && result.blobName) { + setUploadedBlobName(result.blobName); + + if (account?.address) { + addVideo(result.blobName, capturedDescription, capturedPrice, account.address.toString()); + } + + if (onSuccess) { + onSuccess(); + } + + // Reset form + setFile(null); + setDescription(""); + if (fileInputRef.current) fileInputRef.current.value = ""; + toast.success("Video uploaded successfully!"); + } + } catch (e) { + console.error(e); + } + }; + + return ( +
+

+ + Upload New Video +

+ +
+
fileInputRef.current?.click()} + > + + {file ? ( +
+ Selected: {file.name} ({(file.size / (1024 * 1024)).toFixed(2)} MB) +
+ ) : ( +
+ +

Click to select a video file (MP4, WebM)

+
+ )} +
+ + {uploadedBlobName && ( +
+
+ +
+
+

Video Uploaded Successfully!

+

Ready to stream on ShelbyNet

+
+ +
+ )} + +
+
+ + setPrice(e.target.value)} + disabled={isUploading} + className="bg-black/20 border-white/10 text-white placeholder:text-gray-500 focus:border-blue-500/50 disabled:opacity-50" + /> +
+
+ + setDescription(e.target.value)} + placeholder="Short description..." + disabled={isUploading} + className="bg-black/20 border-white/10 text-white placeholder:text-gray-500 focus:border-blue-500/50 disabled:opacity-50" + /> +
+
+ + +
+
+ ); +}; diff --git a/apps/streaming-video/components/WalletProvider.tsx b/apps/streaming-video/components/WalletProvider.tsx new file mode 100644 index 0000000..d33ac41 --- /dev/null +++ b/apps/streaming-video/components/WalletProvider.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { setupAutomaticEthereumWalletDerivation } from "@aptos-labs/derived-wallet-ethereum"; +import { setupAutomaticSolanaWalletDerivation } from "@aptos-labs/derived-wallet-solana"; +import { Network } from "@aptos-labs/ts-sdk"; +import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react"; +import type { PropsWithChildren } from "react"; + +setupAutomaticSolanaWalletDerivation({ defaultNetwork: Network.SHELBYNET }); +setupAutomaticEthereumWalletDerivation({ defaultNetwork: Network.SHELBYNET }); + +export const WalletProvider = ({ children }: PropsWithChildren) => { + return ( + { + console.log("error", error); + }} + > + {children} + + ); +}; diff --git a/apps/streaming-video/hooks/useUploadVideo.tsx b/apps/streaming-video/hooks/useUploadVideo.tsx new file mode 100644 index 0000000..9529943 --- /dev/null +++ b/apps/streaming-video/hooks/useUploadVideo.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { getShelbyClient } from "@/utils/client"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { toast } from "sonner"; +import { + ClayErasureCodingProvider, + expectedTotalChunksets, + generateCommitments, +} from "@shelby-protocol/sdk/browser"; + +export const useUploadVideo = () => { + const { signAndSubmitTransaction, account } = useWallet(); + const [isUploading, setIsUploading] = useState(false); + + const uploadVideo = async (file: File) => { + setIsUploading(true); + try { + if (!account) { + toast.error("Please connect your wallet first!"); + throw new Error("Wallet not connected"); + } + + const client = getShelbyClient(); + + // DẪN CHỨNG: Trên Shelbynet, module nằm tại địa chỉ này + const moduleAddress = "0xc63d6a5efb0080a6029403131715bd4971e1149f7cc099aac69bb0069b3ddbf5"; + + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const blobName = `${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}-${Date.now()}`; + + const provider = await ClayErasureCodingProvider.create(); + const commitments = await generateCommitments(provider, buffer); + + // CHUYỂN ĐỔI: Sử dụng Uint8Array cho vector (Browser compatible) + const cleanHex = commitments.blob_merkle_root.startsWith("0x") + ? commitments.blob_merkle_root.slice(2) + : commitments.blob_merkle_root; + const merkleRootBytes = new Uint8Array( + cleanHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || [] + ); + + // Tính thời gian hết hạn: Đặt time-to-live là 30 ngày (tính bằng microseconds) + const TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days + const expirationMicros = BigInt(Date.now() * 1000) + BigInt(TTL_SECONDS * 1000000); + + // Log để debug + console.log("Debug Upload:", { + moduleAddress, + blobName, + expirationMicros: expirationMicros.toString(), + nowMicros: BigInt(Date.now() * 1000).toString() + }); + + const payload = { + data: { + function: `${moduleAddress}::blob_metadata::register_blob`, + typeArguments: [], + functionArguments: [ + blobName, // 1. String + expirationMicros.toString(), // 2. u64 (Expiration) + merkleRootBytes, // 3. vector (Uint8Array) + expectedTotalChunksets(commitments.raw_data_size), // 4. u32 (Number) + String(commitments.raw_data_size), // 5. u64 (Size) + 0, // 6. u8 (Number) + 0 // 7. u8 (Number) + ], + } + }; + + // Gửi giao dịch + const response = await signAndSubmitTransaction(payload as any); + + // Upload Blob Data lên RPC + await client.rpc.putBlob({ + account: account.address, + blobName, + blobData: new Uint8Array(buffer), + }); + + // Success toast is shown by VideoUploader component + return { blobName, transactionHash: response.hash }; + + } catch (error: any) { + console.error("Execution error:", error); + // In chi tiết lỗi từ Simulation nếu có + const errorMsg = error.data?.message || error.message || "Transaction failed"; + toast.error(errorMsg); + throw error; + } finally { + setIsUploading(false); + } + }; + + return { uploadVideo, isUploading }; +}; \ No newline at end of file diff --git a/apps/streaming-video/hooks/useVideoStorage.ts b/apps/streaming-video/hooks/useVideoStorage.ts new file mode 100644 index 0000000..60ee997 --- /dev/null +++ b/apps/streaming-video/hooks/useVideoStorage.ts @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react'; + +export interface VideoMetadata { + blobName: string; + description: string; + price: string; + timestamp: number; + owner?: string; + url?: string; // Optional: full URL for external videos +} + +const DEFAULT_VIDEOS: VideoMetadata[] = [ + { + blobName: "big_buck_bunny", + description: "Big Buck Bunny - Open Source Animation", + price: "0", + timestamp: Date.now(), + owner: "0x1", + url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + }, + { + blobName: "sintel", + description: "Sintel - Fantasy Short Film", + price: "0", + timestamp: Date.now() - 100000, + owner: "0x1", + url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" + } +]; + +export const useVideoStorage = (trigger?: number) => { + const [videos, setVideos] = useState([]); + + useEffect(() => { + loadVideos(); + }, [trigger]); // Re-load when trigger changes, or on mount + + const loadVideos = () => { + try { + const stored = localStorage.getItem('shelby_videos_v2'); + if (stored) { + const parsed = JSON.parse(stored); + if (Array.isArray(parsed)) { + setVideos(parsed); + } else { + // Invalid format, reset and clean up + setVideos(DEFAULT_VIDEOS); + localStorage.setItem('shelby_videos_v2', JSON.stringify(DEFAULT_VIDEOS)); + } + } else { + setVideos(DEFAULT_VIDEOS); + localStorage.setItem('shelby_videos_v2', JSON.stringify(DEFAULT_VIDEOS)); + } + } catch (e) { + console.error("Storage error:", e); + setVideos(DEFAULT_VIDEOS); + localStorage.setItem('shelby_videos_v2', JSON.stringify(DEFAULT_VIDEOS)); + } + }; + + const addVideo = (blobName: string, description: string, price: string, owner: string) => { + const newVideo: VideoMetadata = { + blobName, + description, + price, + owner, + timestamp: Date.now() + }; + + try { + // Always read fresh state from localStorage to ensure consistency + const currentStored = localStorage.getItem('shelby_videos_v2'); + let currentVideos = currentStored ? JSON.parse(currentStored) : DEFAULT_VIDEOS; + if (!Array.isArray(currentVideos)) currentVideos = DEFAULT_VIDEOS; + + const updatedVideos = [newVideo, ...currentVideos]; + setVideos(updatedVideos); + localStorage.setItem('shelby_videos_v2', JSON.stringify(updatedVideos)); + } catch (e) { + console.error("Storage error in addVideo:", e); + // Fallback: just add to current state + const updatedVideos = [newVideo, ...videos]; + setVideos(updatedVideos); + } + }; + + const removeVideo = (blobName: string) => { + try { + const currentStored = localStorage.getItem('shelby_videos_v2'); + let currentVideos: VideoMetadata[] = currentStored ? JSON.parse(currentStored) : videos; + if (!Array.isArray(currentVideos)) currentVideos = DEFAULT_VIDEOS; + + const updatedVideos = currentVideos.filter(v => v.blobName !== blobName); + setVideos(updatedVideos); + localStorage.setItem('shelby_videos_v2', JSON.stringify(updatedVideos)); + } catch (e) { + console.error("Storage error in removeVideo:", e); + // Fallback: filter from current state + const updatedVideos = videos.filter(v => v.blobName !== blobName); + setVideos(updatedVideos); + } + }; + + return { videos, addVideo, removeVideo }; +}; diff --git a/apps/streaming-video/next-env.d.ts b/apps/streaming-video/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/apps/streaming-video/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/streaming-video/next.config.ts b/apps/streaming-video/next.config.ts new file mode 100644 index 0000000..baf3206 --- /dev/null +++ b/apps/streaming-video/next.config.ts @@ -0,0 +1,26 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + transpilePackages: ["@shelby-protocol/ui"], + typedRoutes: true, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "api.shelbynet.shelby.xyz", + port: "", + pathname: "/**", + }, + ], + }, + async rewrites() { + return [ + { + source: "/api/shelby/:path*", + destination: "https://api.shelbynet.shelby.xyz/shelby/v1/:path*", + }, + ]; + }, +}; + +export default nextConfig; diff --git a/apps/streaming-video/package.json b/apps/streaming-video/package.json new file mode 100644 index 0000000..aa8d5e9 --- /dev/null +++ b/apps/streaming-video/package.json @@ -0,0 +1,32 @@ +{ + "name": "@shelby-protocol/streaming-video", + "version": "0.0.0", + "description": "Shelby Streaming Video example with Gating", + "private": true, + "scripts": { + "dev": "next dev --turbopack --port 3005", + "build": "next build", + "start": "next start", + "lint": "biome check .", + "fmt": "biome check . --write" + }, + "dependencies": { + "@aptos-labs/derived-wallet-ethereum": "^0.8.5", + "@aptos-labs/derived-wallet-solana": "^0.10.0", + "@aptos-labs/ts-sdk": "^5.1.1", + "@aptos-labs/wallet-adapter-react": "^8.1.0", + "@shelby-protocol/sdk": "latest", + "@shelby-protocol/ui": "workspace:*", + "lucide-react": "^0.378.0", + "next": "^15.5.7", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-player": "^2.16.1", + "sonner": "^1.4.41" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1" + } +} \ No newline at end of file diff --git a/apps/streaming-video/postcss.config.mjs b/apps/streaming-video/postcss.config.mjs new file mode 100644 index 0000000..e4b5ceb --- /dev/null +++ b/apps/streaming-video/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from "@shelby-protocol/ui/postcss.config"; diff --git a/apps/streaming-video/tsconfig.json b/apps/streaming-video/tsconfig.json new file mode 100644 index 0000000..1e70252 --- /dev/null +++ b/apps/streaming-video/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"], + "@shelby-protocol/ui/*": ["../../packages/ui/src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/streaming-video/utils/client.ts b/apps/streaming-video/utils/client.ts new file mode 100644 index 0000000..1adb005 --- /dev/null +++ b/apps/streaming-video/utils/client.ts @@ -0,0 +1,31 @@ +import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; +import { ShelbyClient } from "@shelby-protocol/sdk/browser"; + +let aptosClient: Aptos | undefined; +let shelbyClient: ShelbyClient | undefined; + +export const getAptosClient = () => { + if (!aptosClient) { + aptosClient = new Aptos( + new AptosConfig({ + network: Network.CUSTOM, + fullnode: typeof window !== "undefined" ? "/api/shelby" : "https://api.shelbynet.shelby.xyz/v1", + clientConfig: { + API_KEY: process.env.NEXT_PUBLIC_APTOS_API_KEY, + }, + }), + ); + } + return aptosClient; +}; + +export const getShelbyClient = () => { + if (!shelbyClient) { + shelbyClient = new ShelbyClient({ + network: Network.SHELBYNET, + // Only use API key if it exists, otherwise rely on public access for reads + apiKey: process.env.NEXT_PUBLIC_SHELBY_API_KEY || "", + }); + } + return shelbyClient; +}; diff --git a/package.json b/package.json index 6a22f03..d9fb60b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ }, "pnpm": { "overrides": { - "keyv": "npm:@keyvhq/core@~1.6.26" + "keyv": "npm:@keyvhq/core@~1.6.26", + "@aptos-labs/ace-sdk": "npm:noop@latest" }, "onlyBuiltDependencies": [ "@biomejs/biome", @@ -31,4 +32,4 @@ "sharp" ] } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2153ecd..200ab81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: keyv: npm:@keyvhq/core@~1.6.26 + '@aptos-labs/ace-sdk': npm:noop@latest importers: @@ -58,7 +59,7 @@ importers: version: 3.3.2 openai: specifier: ^6.6.0 - version: 6.16.0(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.3.5) + version: 6.16.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.3.5) react: specifier: ^19.1.0 version: 19.2.3 @@ -162,9 +163,6 @@ importers: apps/solana/token-gated: dependencies: - '@aptos-labs/ace-sdk': - specifier: ^0.1.0 - version: 0.1.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@coral-xyz/anchor': specifier: ^0.31.1 version: 0.31.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -176,7 +174,7 @@ importers: version: 0.0.9(@aptos-labs/ts-sdk@5.2.1(got@11.8.6)) '@shelby-protocol/solana-kit': specifier: latest - version: 0.1.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(got@11.8.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 0.1.2(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(got@11.8.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/client': specifier: ^1.2.0 version: 1.6.0(@solana/sysvars@5.4.0(typescript@5.9.3))(@types/react@19.1.0)(bufferutil@4.1.0)(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(utf-8-validate@5.0.10) @@ -242,6 +240,67 @@ importers: specifier: ^5 version: 5.9.3 + apps/streaming-video: + dependencies: + '@aptos-labs/derived-wallet-ethereum': + specifier: ^0.8.5 + version: 0.8.5(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5) + '@aptos-labs/derived-wallet-solana': + specifier: ^0.10.0 + version: 0.10.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@aptos-labs/ts-sdk': + specifier: ^5.1.1 + version: 5.2.1(got@11.8.6) + '@aptos-labs/wallet-adapter-react': + specifier: ^8.1.0 + version: 8.1.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@telegram-apps/bridge@1.9.2)(@types/react@18.3.27)(@wallet-standard/core@1.1.1)(react@19.2.3) + '@shelby-protocol/sdk': + specifier: latest + version: 0.0.9(@aptos-labs/ts-sdk@5.2.1(got@11.8.6)) + '@shelby-protocol/ui': + specifier: workspace:* + version: link:../../packages/ui + dotenv: + specifier: ^16.6.1 + version: 16.6.1 + form-data: + specifier: ^4.0.4 + version: 4.0.5 + lucide-react: + specifier: ^0.378.0 + version: 0.378.0(react@19.2.3) + next: + specifier: ^15.5.7 + version: 15.5.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + openai: + specifier: ^6.6.0 + version: 6.16.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.3.5) + react: + specifier: ^19.1.0 + version: 19.2.3 + react-dom: + specifier: ^19.1.0 + version: 19.2.3(react@19.2.3) + react-player: + specifier: ^2.16.1 + version: 2.16.1(react@19.2.3) + sonner: + specifier: ^1.4.41 + version: 1.7.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + devDependencies: + '@types/node': + specifier: ^22.15.3 + version: 22.19.6 + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) + apps/upload-blob: dependencies: '@aptos-labs/ts-sdk': @@ -385,12 +444,6 @@ packages: '@aptos-labs/wallet-standard': ^0.5.0 '@telegram-apps/bridge': ^1.0.0 - '@aptos-labs/ace-sdk@0.1.0': - resolution: {integrity: sha512-wp3gwgmELU20cF7IWiEx8vVKh8GKK42D0YI+nAf+jBk7b/tYyModMN+wc+/UZQljJdYR+eT4aZmHAN++L/zGuQ==} - peerDependencies: - '@aptos-labs/ts-sdk': ^5.2.0 - '@solana/web3.js': ^1.95.0 - '@aptos-labs/aptos-cli@1.1.1': resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} hasBin: true @@ -406,6 +459,11 @@ packages: peerDependencies: '@aptos-labs/ts-sdk': ^5.1.1 + '@aptos-labs/derived-wallet-base@0.11.0': + resolution: {integrity: sha512-RoI6bWbRQIAkzm+rd9AtgPZ0/jmmYyyTwYtVqjMyzEOIRD9nfMjOKzixVtD+y6H1O/QXpAD4HSTJF4mitLueaQ==} + peerDependencies: + '@aptos-labs/ts-sdk': ^5.1.1 + '@aptos-labs/derived-wallet-ethereum@0.8.5': resolution: {integrity: sha512-mV9cNOzfVSLr8HYMJ1UTrsxGgET/awou+QFY55jtydXD9efaGB3mCTi65Y4oQU8fe4TyF0NNFG9HTnzaZH3RLA==} peerDependencies: @@ -416,8 +474,8 @@ packages: peerDependencies: '@aptos-labs/ts-sdk': ^5.1.1 - '@aptos-labs/derived-wallet-solana@0.9.2': - resolution: {integrity: sha512-jUG+REJ72+f5WmirUiqyiiyKwQSvp+TqOWvrcKkSY4oygzGGpZGXkIrOVm53lA9/iwRk8RbQXHO/pakUCfjSMw==} + '@aptos-labs/derived-wallet-solana@0.12.0': + resolution: {integrity: sha512-9jco25sSBPdYvda8uC3HyFDVMKKzhVrgqbTnEoDelevUm5JG9ueFXJBbBDRu6QQA/OZBP/xlfWDY4VvrBTFeaQ==} peerDependencies: '@aptos-labs/ts-sdk': ^5.1.1 @@ -1716,12 +1774,16 @@ packages: peerDependencies: '@aptos-labs/ts-sdk': ^2.0.1 || ^3.0.0 || ^4.0.0 || ^5.0.0 - '@shelby-protocol/solana-kit@0.1.1': - resolution: {integrity: sha512-qWEjvdH8QSgrmmGZ/s9QWear6XxmzdZKPDpTNBjUbrI9Ug67xAwcgCW9S+NrSXcL1r6y6HBxMzjSeRz9NNfIGw==} + '@shelby-protocol/solana-kit@0.1.2': + resolution: {integrity: sha512-CUsjP4R9LbUhiJyzxtgNeFo/740+PzZDqsupQLnQn1SDbD0vVu2I46u1SogpCqXnW2g/x8E4zYuj4CgRJS37WA==} peerDependencies: - '@solana/web3.js': ^1.98.4 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -2907,6 +2969,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -3675,6 +3741,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + load-script@1.0.0: + resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3696,6 +3765,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.378.0: + resolution: {integrity: sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -3708,6 +3782,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4038,9 +4115,17 @@ packages: peerDependencies: react: ^19.2.3 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-player@2.16.1: + resolution: {integrity: sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==} + peerDependencies: + react: '>=16.6.0' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4202,6 +4287,12 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sonner@1.7.4: + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -4753,14 +4844,6 @@ snapshots: transitivePeerDependencies: - '@wallet-standard/core' - '@aptos-labs/ace-sdk@0.1.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': - dependencies: - '@aptos-labs/ts-sdk': 5.2.1(got@11.8.6) - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 - '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@aptos-labs/aptos-cli@1.1.1': dependencies: commander: 12.1.0 @@ -4776,6 +4859,13 @@ snapshots: transitivePeerDependencies: - '@wallet-standard/core' + '@aptos-labs/derived-wallet-base@0.11.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)': + dependencies: + '@aptos-labs/ts-sdk': 5.2.1(got@11.8.6) + '@aptos-labs/wallet-standard': 0.5.2(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) + transitivePeerDependencies: + - '@wallet-standard/core' + '@aptos-labs/derived-wallet-ethereum@0.8.5(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5)': dependencies: '@aptos-labs/derived-wallet-base': 0.10.1(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) @@ -4810,9 +4900,9 @@ snapshots: - typescript - utf-8-validate - '@aptos-labs/derived-wallet-solana@0.9.2(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@aptos-labs/derived-wallet-solana@0.12.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@aptos-labs/derived-wallet-base': 0.10.1(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) + '@aptos-labs/derived-wallet-base': 0.11.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) '@aptos-labs/ts-sdk': 5.2.1(got@11.8.6) '@aptos-labs/wallet-standard': 0.5.2(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) '@solana/wallet-adapter-base': 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -4820,6 +4910,7 @@ snapshots: '@solana/wallet-standard-wallet-adapter-base': 1.1.4(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bs58@6.0.0) '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@wallet-standard/app': 1.1.0 + tweetnacl: 1.0.3 transitivePeerDependencies: - '@wallet-standard/core' - bs58 @@ -5993,18 +6084,18 @@ snapshots: p-limit: 7.1.1 zod: 3.25.76 - '@shelby-protocol/solana-kit@0.1.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(got@11.8.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@shelby-protocol/solana-kit@0.1.2(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(got@11.8.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@aptos-labs/derived-wallet-base': 0.10.1(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) - '@aptos-labs/derived-wallet-solana': 0.9.2(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@aptos-labs/derived-wallet-solana': 0.12.0(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1)(bs58@6.0.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@aptos-labs/gas-station-client': 2.0.3(got@11.8.6) '@aptos-labs/ts-sdk': 5.2.1(got@11.8.6) + '@aptos-labs/wallet-standard': 0.5.2(@aptos-labs/ts-sdk@5.2.1(got@11.8.6))(@wallet-standard/core@1.1.1) '@shelby-protocol/sdk': 0.0.9(@aptos-labs/ts-sdk@5.2.1(got@11.8.6)) - '@solana/wallet-standard-util': 1.1.2 + '@solana/wallet-standard-features': 1.3.0 '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + optionalDependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - tweetnacl: 1.0.3 transitivePeerDependencies: - '@wallet-standard/core' - bs58 @@ -7352,6 +7443,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + defer-to-connect@2.0.1: {} define-data-property@1.1.4: @@ -8272,6 +8365,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + load-script@1.0.0: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -8290,6 +8385,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.378.0(react@19.2.3): + dependencies: + react: 19.2.3 + lucide-react@0.544.0(react@19.2.3): dependencies: react: 19.2.3 @@ -8300,6 +8399,8 @@ snapshots: math-intrinsics@1.1.0: {} + memoize-one@5.2.1: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -8469,9 +8570,9 @@ snapshots: dependencies: wrappy: 1.0.2 - openai@6.16.0(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.3.5): + openai@6.16.0(ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(zod@4.3.5): optionalDependencies: - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) zod: 4.3.5 optionator@0.9.4: @@ -8595,8 +8696,19 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-fast-compare@3.2.2: {} + react-is@16.13.1: {} + react-player@2.16.1(react@19.2.3): + dependencies: + deepmerge: 4.3.1 + load-script: 1.0.0 + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 19.2.3 + react-fast-compare: 3.2.2 + react-remove-scroll-bar@2.3.8(@types/react@18.3.27)(react@19.2.3): dependencies: react: 19.2.3 @@ -8843,6 +8955,11 @@ snapshots: sisteransi@1.0.5: {} + sonner@1.7.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3