diff --git a/.gitignore b/.gitignore index 6af8696..de20e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log yarn-error.log yarn-debug.log .env +tsconfig.tsbuildinfo # Build Outputs .next/ diff --git a/README.md b/README.md index d880f97..382d0dd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ This Turborepo includes the following example applications: | `@shelby-protocol/ai-image-generation` | Shelby AI image generation example | [`apps/ai-image-generation`](./apps/ai-image-generation) | | `@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-examples/ethereum-file-upload` | No description provided | [`apps/ethereum/file-upload`](./apps/ethereum/file-upload) | | `@shelby-protocol/list-example` | An example app to demonstrate listing blobs using the Shelby SDK | [`apps/list-blob`](./apps/list-blob) | +| `token-gated` | Next.js, Tailwind, @solana/react-hooks, Anchor vault program | [`apps/solana/token-gated`](./apps/solana/token-gated) | +| `@shelby-protocol/solana-example` | Shelby solana-kit example - full blob storage flow with Solana wallets | [`apps/solana-example`](./apps/solana-example) | | `@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/simple-example/README.md b/apps/solana/simple-example/README.md new file mode 100644 index 0000000..cc0b646 --- /dev/null +++ b/apps/solana/simple-example/README.md @@ -0,0 +1,375 @@ +# Shelby Solana Kit Example + +This example demonstrates how to use `@shelby-protocol/solana-kit` to upload blobs to Shelby storage using Solana keypairs. + +## Overview + +The Solana Kit provides a native Solana integration for Shelby's decentralized storage. It allows you to: + +1. Create storage accounts derived from Solana keypairs +2. Fund accounts with ShelbyUSD (upload fees) and APT (transaction fees) +3. Upload blobs to Shelby storage +4. Delete blobs from Shelby storage + +## How It Works + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 1. Connect │───▶│ 2. Create Storage│───▶│ 3. Fund with │───▶│ 4. Upload/Delete│ +│ Wallet │ │ Account │ │ ShelbyUSD + APT │ │ Blobs │ +└─────────────┘ └──────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### Key Concepts + +- **Storage Account**: A Shelby account derived from a Solana keypair and a domain. The domain acts as a namespace for your blobs. +- **ShelbyUSD**: The token used to pay for storage fees. +- **APT**: Used to pay for transaction fees on the Shelby network. + +## Project Structure + +``` +app/ +├── page.tsx # Main orchestrator +├── api/ +│ ├── create-storage-account/ # Create storage account +│ ├── fund-account/ # Fund with ShelbyUSD + APT +│ ├── upload-blob/ # Upload blob +│ └── delete-blob/ # Delete blob +components/ +├── WalletProvider.tsx # Solana wallet setup +├── StorageAccountManager.tsx # Keypair, domain, create, fund +└── BlobUploader.tsx # Upload, list, delete blobs +hooks/ +├── useCreateStorageAccount.ts # Create storage account hook +├── useFundAccount.ts # Fund account hook +├── useUploadBlob.ts # Upload blob hook +└── useDeleteBlob.ts # Delete blob hook +utils/ +└── client.ts # Singleton Shelby client +``` + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- pnpm +- A Solana wallet (like Phantom or Solflare) + +### Environment Variables + +Create a `.env.local` file: + +```env +SHELBY_API_KEY=your_api_key +NEXT_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com +``` + +### Installation + +```bash +# From the monorepo root +pnpm install + +# Run the development server +pnpm --filter @shelby-protocol/solana-kit dev +``` + +The app will be available at [http://localhost:3002](http://localhost:3002). + +## Usage Flow + +1. **Connect Wallet**: Connect your Solana wallet (Phantom, Solflare, etc.) + +2. **Create Storage Account**: + - A new Solana keypair is generated + - Enter a domain for your storage namespace + - The storage account address is derived from the keypair + domain + +3. **Fund Account**: + - Fund with ShelbyUSD for upload fees + - Fund with APT for transaction fees + - Uses Shelby devnet faucets for development + +4. **Upload Blobs**: + - Select a file to upload + - The blob is uploaded to Shelby devnet + - Get a URL for your blob + +5. **Delete Blobs**: + - Remove blobs you no longer need + - Transaction is submitted to the Shelby network + +## API Routes Reference + +### Create Storage Account + +`POST /api/create-storage-account` + +Creates a storage account derived from a Solana keypair and domain. + +```typescript +// app/api/create-storage-account/route.ts +const storageAccount = shelbyClient.createStorageAccount(keypair, domain); + +// Response +{ + success: true, + storageAccountAddress: "0x...", + solanaPublicKey: "..." +} +``` + +### Fund Account + +`POST /api/fund-account` + +Funds the storage account with ShelbyUSD and/or APT tokens. + +```typescript +// app/api/fund-account/route.ts +await shelbyClient.fundAccountWithShelbyUSD({ + address: storageAccount.accountAddress, + amount: shelbyUsdAmount, +}); + +await shelbyClient.fundAccountWithAPT({ + address: storageAccount.accountAddress, + amount: aptAmount, +}); + +// Response +{ + success: true, + storageAccountAddress: "0x...", + funded: { shelbyUsd: true, apt: true } +} +``` + +### Upload Blob + +`POST /api/upload-blob` + +Uploads blob data to Shelby storage. + +```typescript +// app/api/upload-blob/route.ts +const expirationMicros = Date.now() * 1000 + expirationDays * 24 * 60 * 60 * 1000 * 1000; + +await shelbyClient.upload({ + blobData: new Uint8Array(blobData), + signer: storageAccount, + blobName, + expirationMicros, +}); + +const blobUrl = `https://api.shelbynet.shelby.xyz/shelby/v1/blobs/${storageAccount.accountAddress}/${blobName}`; + +// Response +{ + success: true, + blobName: "example.txt", + blobUrl: "https://api.shelbynet.shelby.xyz/shelby/v1/blobs/0x.../example.txt", + storageAccountAddress: "0x...", + expirationMicros: 1234567890000000 +} +``` + +### Delete Blob + +`POST /api/delete-blob` + +Deletes a blob from Shelby storage. + +```typescript +// app/api/delete-blob/route.ts +import { ShelbyBlobClient } from "@shelby-protocol/sdk/node"; + +// Create delete payload +const payload = ShelbyBlobClient.createDeleteBlobPayload({ + blobNameSuffix: blobName, +}); + +// Build, sign, and submit transaction +const transaction = await shelbyClient.aptos.transaction.build.simple({ + sender: storageAccount.accountAddress, + data: payload, +}); + +const authenticator = storageAccount.signTransactionWithAuthenticator(transaction); +const response = await shelbyClient.aptos.transaction.submit.simple({ + transaction, + senderAuthenticator: authenticator, +}); + +await shelbyClient.aptos.waitForTransaction({ + transactionHash: response.hash, +}); + +// Response +{ + success: true, + blobName: "example.txt", + transactionHash: "0x..." +} +``` + +## Custom Hooks + +React hooks for interacting with Shelby storage from client components. + +### useCreateStorageAccount + +```typescript +import { useCreateStorageAccount } from "@/hooks/useCreateStorageAccount"; + +const { createStorageAccount, isCreating, error } = useCreateStorageAccount(); + +const result = await createStorageAccount(keypair.secretKey, "my-domain.com"); +// result: { storageAccountAddress: string, solanaPublicKey: string } +``` + +### useFundAccount + +```typescript +import { useFundAccount } from "@/hooks/useFundAccount"; + +const { fundAccount, isFunding, error } = useFundAccount(); + +const result = await fundAccount( + keypair.secretKey, + "my-domain.com", + 1_000_000_000, // ShelbyUSD amount + 1_000_000_000 // APT amount +); +// result: { storageAccountAddress: string, funded: { shelbyUsd?: boolean, apt?: boolean } } +``` + +### useUploadBlob + +```typescript +import { useUploadBlob } from "@/hooks/useUploadBlob"; + +const { uploadBlob, isUploading, error } = useUploadBlob(); + +const result = await uploadBlob( + keypair.secretKey, + "my-domain.com", + "file.txt", + new Uint8Array([1, 2, 3]), + 7 // expiration days (default: 1) +); +// result: { blobName: string, blobUrl: string, storageAccountAddress: string, expirationMicros: number } +``` + +### useDeleteBlob + +```typescript +import { useDeleteBlob } from "@/hooks/useDeleteBlob"; + +const { deleteBlob, isDeleting } = useDeleteBlob(); + +const result = await deleteBlob(keypair.secretKey, "my-domain.com", "file.txt"); +// result: { blobName: string, transactionHash: string } +``` + +## Code Example + +```typescript +import { Network, Shelby } from "@shelby-protocol/solana-kit/node"; +import { ShelbyBlobClient } from "@shelby-protocol/sdk/node"; +import { Connection, Keypair } from "@solana/web3.js"; + +// Create a Shelby client +const connection = new Connection("https://api.devnet.solana.com"); +const shelbyClient = new Shelby({ + network: Network.SHELBYNET, + connection, + apiKey: process.env.SHELBY_API_KEY, +}); + +// Generate a Solana keypair +const solanaKeypair = Keypair.generate(); + +// Create a storage account +const domain = "my-awesome-dapp.com"; +const storageAccount = shelbyClient.createStorageAccount(solanaKeypair, domain); + +// Fund the account +await shelbyClient.fundAccountWithShelbyUSD({ + address: storageAccount.accountAddress, + amount: 1_000_000_000, // 1 ShelbyUSD +}); + +await shelbyClient.fundAccountWithAPT({ + address: storageAccount.accountAddress, + amount: 1_000_000_000, // 1 APT +}); + +// Upload a blob +const blobName = "example.txt"; +await shelbyClient.upload({ + blobData: new Uint8Array([1, 2, 3]), + signer: storageAccount, + blobName, + expirationMicros: Date.now() * 1000 + 86400000000, // 1 day +}); + +// Blob URL +const blobUrl = `https://api.shelbynet.shelby.xyz/shelby/v1/blobs/${storageAccount.accountAddress}/${blobName}`; + +// Delete a blob +const payload = ShelbyBlobClient.createDeleteBlobPayload({ + blobNameSuffix: blobName, +}); + +const transaction = await shelbyClient.aptos.transaction.build.simple({ + sender: storageAccount.accountAddress, + data: payload, +}); + +const authenticator = storageAccount.signTransactionWithAuthenticator(transaction); +const response = await shelbyClient.aptos.transaction.submit.simple({ + transaction, + senderAuthenticator: authenticator, +}); + +await shelbyClient.aptos.waitForTransaction({ + transactionHash: response.hash, +}); +``` + +## Architecture + +This example uses a server-side architecture for Shelby operations: + +- **Browser**: Handles wallet connection and UI +- **API Routes**: Execute Shelby operations using `@shelby-protocol/solana-kit/node` + +``` +Browser (React Components) + │ + │ fetch() + ▼ +API Routes (/api/*) + │ + │ Shelby SDK + ▼ +Shelby Client (getShelbyClient singleton) + │ + │ HTTPS + ▼ +Shelby Network (SHELBYNET) +``` + +## Related Examples + +- [cross-chain-accounts](../cross-chain-accounts) - Cross-chain wallet derivation example +- [upload-blob](../upload-blob) - Simple blob upload CLI example +- [download-blob](../download-blob) - Blob download CLI example + +## Resources + +- [Shelby Documentation](https://docs.shelby.xyz) +- [Solana Kit Documentation](https://docs.shelby.xyz/sdks/solana-kit) diff --git a/apps/solana/simple-example/app/api/create-storage-account/route.ts b/apps/solana/simple-example/app/api/create-storage-account/route.ts new file mode 100644 index 0000000..7301ab4 --- /dev/null +++ b/apps/solana/simple-example/app/api/create-storage-account/route.ts @@ -0,0 +1,38 @@ +import { Keypair } from "@solana/web3.js"; +import { NextResponse } from "next/server"; +import { getShelbyClient } from "@/utils/client"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { secretKey, domain } = body; + + if (!secretKey || !domain) { + return NextResponse.json( + { error: "Missing secretKey or domain" }, + { status: 400 }, + ); + } + + // Reconstruct the keypair from the secret key + const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey)); + + // Get the Shelby client + const shelbyClient = getShelbyClient(); + + // Create a storage account controlled by the Solana account + const storageAccount = shelbyClient.createStorageAccount(keypair, domain); + + return NextResponse.json({ + success: true, + storageAccountAddress: storageAccount.accountAddress.toString(), + solanaPublicKey: keypair.publicKey.toString(), + }); + } catch (error) { + console.error("Error creating storage account:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 }, + ); + } +} diff --git a/apps/solana/simple-example/app/api/delete-blob/route.ts b/apps/solana/simple-example/app/api/delete-blob/route.ts new file mode 100644 index 0000000..0ae7e07 --- /dev/null +++ b/apps/solana/simple-example/app/api/delete-blob/route.ts @@ -0,0 +1,60 @@ +import { ShelbyBlobClient } from "@shelby-protocol/sdk/node"; +import { Keypair } from "@solana/web3.js"; +import { NextResponse } from "next/server"; +import { getShelbyClient } from "@/utils/client"; + +export async function POST(request: Request) { + try { + const { secretKey, domain, blobName } = await request.json(); + + if (!secretKey || !domain || !blobName) { + return NextResponse.json( + { error: "Missing required fields: secretKey, domain, blobName" }, + { status: 400 }, + ); + } + + // Reconstruct the keypair from the secret key + const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey)); + + // Get the Shelby client + const shelbyClient = getShelbyClient(); + + // Create the storage account reference + const storageAccount = shelbyClient.createStorageAccount(keypair, domain); + + // Create delete payload + const payload = ShelbyBlobClient.createDeleteBlobPayload({ + blobNameSuffix: blobName, + }); + + // Build, sign, and submit transaction + const transaction = await shelbyClient.aptos.transaction.build.simple({ + sender: storageAccount.accountAddress, + data: payload, + }); + + const authenticator = + storageAccount.signTransactionWithAuthenticator(transaction); + const response = await shelbyClient.aptos.transaction.submit.simple({ + transaction, + senderAuthenticator: authenticator, + }); + + await shelbyClient.aptos.waitForTransaction({ + transactionHash: response.hash, + }); + + return NextResponse.json({ + success: true, + blobName, + transactionHash: response.hash, + }); + } catch (error) { + console.error("Error deleting blob:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 }, + ); + } +} diff --git a/apps/solana/simple-example/app/api/fund-account/route.ts b/apps/solana/simple-example/app/api/fund-account/route.ts new file mode 100644 index 0000000..aee65b8 --- /dev/null +++ b/apps/solana/simple-example/app/api/fund-account/route.ts @@ -0,0 +1,88 @@ +import { Keypair } from "@solana/web3.js"; +import { NextResponse } from "next/server"; +import { getShelbyClient } from "@/utils/client"; + +function extractErrorDetails(error: unknown): { + message: string; + code?: string; +} { + if (error instanceof Error) { + const message = error.message; + return { message }; + } + return { message: String(error) }; +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { secretKey, domain, shelbyUsdAmount, aptAmount } = body; + + if (!secretKey || !domain) { + return NextResponse.json( + { error: "Missing secretKey or domain" }, + { status: 400 }, + ); + } + + // Reconstruct the keypair from the secret key + const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey)); + + // Get the Shelby client + const shelbyClient = getShelbyClient(); + + // Create the storage account reference + const storageAccount = shelbyClient.createStorageAccount(keypair, domain); + + const results: { shelbyUsd?: boolean; apt?: boolean } = {}; + + // Fund with ShelbyUSD and APT in parallel for better performance + const fundingPromises: Promise[] = []; + + if (shelbyUsdAmount && shelbyUsdAmount > 0) { + fundingPromises.push( + shelbyClient + .fundAccountWithShelbyUSD({ + address: storageAccount.accountAddress, + amount: shelbyUsdAmount, + }) + .then(() => { + results.shelbyUsd = true; + }), + ); + } + + if (aptAmount && aptAmount > 0) { + fundingPromises.push( + shelbyClient + .fundAccountWithAPT({ + address: storageAccount.accountAddress, + amount: aptAmount, + }) + .then(() => { + results.apt = true; + }), + ); + } + + await Promise.all(fundingPromises); + + return NextResponse.json({ + success: true, + storageAccountAddress: storageAccount.accountAddress.toString(), + funded: results, + }); + } catch (error) { + const errorInfo = extractErrorDetails(error); + console.error("Error funding account:", { + message: errorInfo.message, + code: errorInfo.code, + stack: error instanceof Error ? error.stack : undefined, + }); + + return NextResponse.json( + { error: errorInfo.message, code: errorInfo.code }, + { status: 500 }, + ); + } +} diff --git a/apps/solana/simple-example/app/api/upload-blob/route.ts b/apps/solana/simple-example/app/api/upload-blob/route.ts new file mode 100644 index 0000000..9e930a7 --- /dev/null +++ b/apps/solana/simple-example/app/api/upload-blob/route.ts @@ -0,0 +1,58 @@ +import { Keypair } from "@solana/web3.js"; +import { NextResponse } from "next/server"; +import { getShelbyClient } from "@/utils/client"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { secretKey, domain, blobName, blobData, expirationDays = 1 } = body; + + if (!secretKey || !domain || !blobName || !blobData) { + return NextResponse.json( + { + error: + "Missing required fields: secretKey, domain, blobName, blobData", + }, + { status: 400 }, + ); + } + + // Reconstruct the keypair from the secret key + const keypair = Keypair.fromSecretKey(new Uint8Array(secretKey)); + + // Get the Shelby client + const shelbyClient = getShelbyClient(); + + // Create the storage account reference + const storageAccount = shelbyClient.createStorageAccount(keypair, domain); + + // Calculate expiration in microseconds + const expirationMicros = + Date.now() * 1000 + expirationDays * 24 * 60 * 60 * 1000 * 1000; + + // Upload the blob + await shelbyClient.upload({ + blobData: new Uint8Array(blobData), + signer: storageAccount, + blobName, + expirationMicros, + }); + + // Construct the blob URL + const blobUrl = `https://api.shelbynet.shelby.xyz/shelby/v1/blobs/${storageAccount.accountAddress.toString()}/${blobName}`; + + return NextResponse.json({ + success: true, + blobName, + blobUrl, + storageAccountAddress: storageAccount.accountAddress.toString(), + expirationMicros, + }); + } catch (error) { + console.error("Error uploading blob:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 }, + ); + } +} diff --git a/apps/solana/simple-example/app/fonts/GeistMonoVF.woff b/apps/solana/simple-example/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/apps/solana/simple-example/app/fonts/GeistMonoVF.woff differ diff --git a/apps/solana/simple-example/app/fonts/GeistVF.woff b/apps/solana/simple-example/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/apps/solana/simple-example/app/fonts/GeistVF.woff differ diff --git a/apps/solana/simple-example/app/globals.css b/apps/solana/simple-example/app/globals.css new file mode 100644 index 0000000..ed1f509 --- /dev/null +++ b/apps/solana/simple-example/app/globals.css @@ -0,0 +1,970 @@ +@import "@shelby-protocol/ui/globals.css"; + +/* === POLINE COLOR PALETTE === */ +:root { + /* Accent colors */ + --poline-accent-1: hsl(340, 85.00%, 55.00%); + --poline-accent-2: hsl(326.79, 81.17%, 48.43%); + --poline-accent-3: hsl(313.2, 77.93%, 45.36%); + --poline-accent-4: hsl(303.44, 75.76%, 44.86%); + --poline-accent-5: hsl(300, 75.00%, 45.00%); + --poline-accent-6: hsl(294.3, 74.62%, 40.69%); + --poline-accent-7: hsl(270.63, 73.54%, 31.73%); + --poline-accent-8: hsl(224.42, 71.91%, 33.86%); + --poline-accent-9: hsl(195, 70.00%, 55.00%); + + /* Surface colors */ + --poline-surface-1: hsl(280, 8.00%, 10.00%); + --poline-surface-2: hsl(280, 7.00%, 14.00%); + --poline-surface-3: hsl(280, 6.27%, 16.93%); + --poline-surface-4: hsl(280, 6.00%, 18.00%); + --poline-surface-5: hsl(280, 5.73%, 18.94%); + --poline-surface-6: hsl(280, 5.00%, 21.50%); + --poline-surface-7: hsl(280, 4.00%, 25.00%); + + /* Text colors */ + --poline-text-1: hsl(280, 2.00%, 95.00%); + --poline-text-2: hsl(280, 2.50%, 80.00%); + --poline-text-3: hsl(280, 2.87%, 69.02%); + --poline-text-4: hsl(280, 3.00%, 65.00%); + --poline-text-5: hsl(280, 3.00%, 62.32%); + --poline-text-6: hsl(280, 3.00%, 55.00%); + --poline-text-7: hsl(280, 3.00%, 45.00%); +} + +/* === NEON COLOR VARIABLES === */ +:root { + --neon-pink: oklch(0.7 0.3 340); + --neon-cyan: oklch(0.75 0.2 195); + --neon-lime: oklch(0.8 0.3 130); + --neon-purple: oklch(0.6 0.3 300); + --text-disabled: var(--poline-text-7, oklch(0.45 0.03 280)); + --success: oklch(0.7 0.2 145); +} + +/* Override design system with poline neon palette */ +.dark { + /* Core backgrounds - purple-tinted darks */ + --background: var(--poline-surface-1); + --card: var(--poline-surface-2); + --muted: var(--poline-surface-3); + + /* Text colors - purple-tinted for consistency */ + --foreground: var(--poline-text-1); + --muted-foreground: var(--poline-text-4); + + /* Borders */ + --border: oklch(0.28 0.03 280); + --input: oklch(0.28 0.03 280); + + /* Semantic colors */ + --destructive: oklch(0.65 0.25 25); +} + +/* Surface utilities for inner card backgrounds */ +.surface-elevated { + background: var(--poline-surface-5); +} + +.surface-inset { + background: var(--poline-surface-2); +} + +/* Tailwind utilities */ +@theme inline { + --color-surface-elevated: var(--poline-surface-5); + --color-surface-inset: var(--poline-surface-2); + --color-text-disabled: var(--text-disabled); + --color-success: var(--success); + --color-neon-pink: var(--neon-pink); + --color-neon-cyan: var(--neon-cyan); +} + +/* === ANIMATED BACKGROUND BLOBS === */ +.blob-container { + position: fixed; + inset: 0; + overflow: hidden; + z-index: -1; + pointer-events: none; +} + +.blob { + position: absolute; + filter: blur(80px); + opacity: 0.6; + mix-blend-mode: screen; +} + +.blob-1 { + width: 750px; + height: 680px; + border-radius: 60% 40% 55% 45% / 50% 60% 40% 50%; + background: radial-gradient( + ellipse, + oklch(0.72 0.26 350 / 0.5), + transparent 70% + ); + top: -10%; + left: -12%; + animation: + blob-float-1 20s ease-in-out infinite, + blob-pulse 4s ease-in-out infinite, + blob-morph-1 15s ease-in-out infinite; +} + +.blob-2 { + width: 620px; + height: 680px; + border-radius: 45% 55% 60% 40% / 55% 45% 50% 50%; + background: radial-gradient( + ellipse, + oklch(0.68 0.28 310 / 0.48), + transparent 70% + ); + top: 48%; + right: -8%; + animation: + blob-float-2 25s ease-in-out infinite, + blob-pulse 5s ease-in-out infinite 1s, + blob-morph-2 18s ease-in-out infinite; +} + +.blob-3 { + width: 580px; + height: 520px; + border-radius: 50% 50% 45% 55% / 60% 40% 55% 45%; + background: radial-gradient( + ellipse, + oklch(0.75 0.2 195 / 0.5), + transparent 70% + ); + bottom: -12%; + left: 48%; + animation: + blob-float-3 22s ease-in-out infinite, + blob-pulse 6s ease-in-out infinite 2s, + blob-morph-3 16s ease-in-out infinite; +} + +.blob-4 { + width: 650px; + height: 580px; + border-radius: 55% 45% 40% 60% / 45% 55% 60% 40%; + background: radial-gradient( + ellipse, + oklch(0.7 0.28 330 / 0.5), + transparent 70% + ); + top: -8%; + right: 32%; + animation: + blob-float-4 24s ease-in-out infinite, + blob-pulse 5.5s ease-in-out infinite 0.5s, + blob-morph-1 19s ease-in-out infinite reverse; +} + +.blob-5 { + width: 520px; + height: 580px; + border-radius: 40% 60% 50% 50% / 55% 45% 55% 45%; + background: radial-gradient( + ellipse, + oklch(0.68 0.22 210 / 0.5), + transparent 70% + ); + top: 62%; + left: -6%; + animation: + blob-float-5 28s ease-in-out infinite, + blob-pulse 7s ease-in-out infinite 3s, + blob-morph-2 17s ease-in-out infinite reverse; +} + +.blob-6 { + width: 550px; + height: 480px; + border-radius: 55% 45% 55% 45% / 40% 60% 45% 55%; + background: radial-gradient( + ellipse, + oklch(0.68 0.26 290 / 0.6), + transparent 70% + ); + bottom: 22%; + right: -8%; + animation: + blob-float-6 21s ease-in-out infinite, + blob-pulse 4.5s ease-in-out infinite 1.5s, + blob-morph-3 14s ease-in-out infinite; +} + +.blob-7 { + width: 480px; + height: 420px; + border-radius: 45% 55% 50% 50% / 50% 50% 55% 45%; + background: radial-gradient( + ellipse, + oklch(0.72 0.18 180 / 0.55), + transparent 70% + ); + top: 12%; + left: 52%; + animation: + blob-float-7 26s ease-in-out infinite, + blob-pulse 6.5s ease-in-out infinite 2.5s, + blob-morph-1 20s ease-in-out infinite; +} + +.blob-8 { + width: 540px; + height: 600px; + border-radius: 50% 50% 40% 60% / 60% 40% 50% 50%; + background: radial-gradient( + ellipse, + oklch(0.7 0.3 340 / 0.5), + transparent 70% + ); + bottom: 42%; + left: 22%; + animation: + blob-float-8 23s ease-in-out infinite, + blob-pulse 5s ease-in-out infinite 2s, + blob-morph-2 21s ease-in-out infinite; +} + +.blob-9 { + width: 620px; + height: 550px; + border-radius: 60% 40% 45% 55% / 45% 55% 50% 50%; + background: radial-gradient( + ellipse, + oklch(0.65 0.28 320 / 0.6), + transparent 70% + ); + top: 76%; + left: 28%; + animation: + blob-float-9 27s ease-in-out infinite, + blob-pulse 6s ease-in-out infinite 1s, + blob-morph-3 18s ease-in-out infinite reverse; +} + +.blob-10 { + width: 590px; + height: 520px; + border-radius: 52% 48% 58% 42% / 46% 54% 48% 52%; + background: radial-gradient( + ellipse, + oklch(0.6 0.3 300 / 0.5), + transparent 70% + ); + top: 8%; + right: -6%; + animation: + blob-float-10 29s ease-in-out infinite, + blob-pulse 5.5s ease-in-out infinite 0.8s, + blob-morph-1 22s ease-in-out infinite; +} + +.blob-11 { + width: 500px; + height: 560px; + border-radius: 48% 52% 44% 56% / 58% 42% 54% 46%; + background: radial-gradient( + ellipse, + oklch(0.66 0.26 310 / 0.55), + transparent 70% + ); + bottom: -8%; + left: -3%; + animation: + blob-float-11 24s ease-in-out infinite, + blob-pulse 6s ease-in-out infinite 1.8s, + blob-morph-2 19s ease-in-out infinite reverse; +} + +.blob-12 { + width: 500px; + height: 520px; + border-radius: 52% 48% 46% 54% / 48% 52% 56% 44%; + background: radial-gradient( + ellipse, + oklch(0.68 0.24 280 / 0.45), + transparent 70% + ); + top: 18%; + left: -12%; + animation: + blob-float-12 26s ease-in-out infinite, + blob-pulse 5.5s ease-in-out infinite 1.2s, + blob-morph-1 20s ease-in-out infinite; +} + +.blob-13 { + width: 480px; + height: 500px; + border-radius: 46% 54% 52% 48% / 54% 46% 48% 52%; + background: radial-gradient( + ellipse, + oklch(0.64 0.26 300 / 0.45), + transparent 70% + ); + top: 45%; + left: -9%; + animation: + blob-float-13 23s ease-in-out infinite, + blob-pulse 6s ease-in-out infinite 2.2s, + blob-morph-3 17s ease-in-out infinite reverse; +} + +.blob-14 { + width: 560px; + height: 540px; + border-radius: 54% 46% 48% 52% / 52% 48% 46% 54%; + background: radial-gradient( + ellipse, + oklch(0.72 0.2 195 / 0.5), + transparent 70% + ); + top: 35%; + right: 35%; + animation: + blob-float-7 25s ease-in-out infinite reverse, + blob-pulse 5s ease-in-out infinite 1.5s, + blob-morph-2 16s ease-in-out infinite; +} + +.blob-15 { + width: 600px; + height: 620px; + border-radius: 48% 52% 55% 45% / 45% 55% 48% 52%; + background: radial-gradient( + ellipse, + oklch(0.74 0.22 160 / 0.55), + transparent 70% + ); + bottom: 32%; + right: 22%; + animation: + blob-float-9 22s ease-in-out infinite, + blob-pulse 6.5s ease-in-out infinite 2s, + blob-morph-1 18s ease-in-out infinite reverse; +} + +@keyframes blob-float-1 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg); + } + 25% { + transform: translate(50px, 30px) rotate(5deg); + } + 50% { + transform: translate(20px, 60px) rotate(-5deg); + } + 75% { + transform: translate(-30px, 40px) rotate(3deg); + } +} + +@keyframes blob-float-2 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(-40px, -30px) scale(1.05); + } + 66% { + transform: translate(30px, 20px) scale(0.95); + } +} + +@keyframes blob-float-3 { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(-60px, -40px); + } +} + +@keyframes blob-float-4 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg); + } + 20% { + transform: translate(-30px, 40px) rotate(-3deg); + } + 40% { + transform: translate(-50px, 20px) rotate(2deg); + } + 60% { + transform: translate(-20px, 50px) rotate(-4deg); + } + 80% { + transform: translate(20px, 30px) rotate(3deg); + } +} + +@keyframes blob-float-5 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 25% { + transform: translate(40px, -20px) scale(1.03); + } + 50% { + transform: translate(60px, 30px) scale(0.97); + } + 75% { + transform: translate(30px, -40px) scale(1.02); + } +} + +@keyframes blob-float-6 { + 0%, + 100% { + transform: translate(0, 0); + } + 33% { + transform: translate(-35px, -25px); + } + 66% { + transform: translate(25px, -45px); + } +} + +@keyframes blob-float-7 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg) scale(1); + } + 30% { + transform: translate(30px, 35px) rotate(4deg) scale(1.04); + } + 60% { + transform: translate(-25px, 20px) rotate(-3deg) scale(0.96); + } +} + +@keyframes blob-float-8 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg); + } + 25% { + transform: translate(45px, -25px) rotate(3deg); + } + 50% { + transform: translate(25px, 35px) rotate(-2deg); + } + 75% { + transform: translate(-20px, 15px) rotate(4deg); + } +} + +@keyframes blob-float-9 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 20% { + transform: translate(-35px, 20px) scale(1.03); + } + 40% { + transform: translate(-15px, -30px) scale(0.98); + } + 60% { + transform: translate(25px, -15px) scale(1.02); + } + 80% { + transform: translate(40px, 25px) scale(0.97); + } +} + +@keyframes blob-float-10 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg); + } + 20% { + transform: translate(-25px, 35px) rotate(-2deg); + } + 45% { + transform: translate(30px, 50px) rotate(3deg); + } + 70% { + transform: translate(45px, -20px) rotate(-3deg); + } + 90% { + transform: translate(-15px, -30px) rotate(2deg); + } +} + +@keyframes blob-float-11 { + 0%, + 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + } + 30% { + transform: translate(55px, 25px) scale(1.04) rotate(4deg); + } + 60% { + transform: translate(35px, -35px) scale(0.97) rotate(-2deg); + } + 85% { + transform: translate(-20px, 15px) scale(1.02) rotate(3deg); + } +} + +@keyframes blob-float-12 { + 0%, + 100% { + transform: translate(0, 0) rotate(0deg) scale(1); + } + 25% { + transform: translate(45px, 30px) rotate(3deg) scale(1.03); + } + 50% { + transform: translate(30px, -25px) rotate(-2deg) scale(0.98); + } + 75% { + transform: translate(-15px, 20px) rotate(4deg) scale(1.02); + } +} + +@keyframes blob-float-13 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 20% { + transform: translate(35px, -20px) scale(1.04); + } + 45% { + transform: translate(50px, 25px) scale(0.97); + } + 70% { + transform: translate(20px, 40px) scale(1.02); + } + 90% { + transform: translate(-10px, 15px) scale(0.99); + } +} + +@keyframes blob-morph-1 { + 0%, + 100% { + border-radius: 60% 40% 55% 45% / 50% 60% 40% 50%; + } + 25% { + border-radius: 45% 55% 50% 50% / 55% 45% 55% 45%; + } + 50% { + border-radius: 55% 45% 40% 60% / 45% 55% 60% 40%; + } + 75% { + border-radius: 50% 50% 60% 40% / 60% 40% 45% 55%; + } +} + +@keyframes blob-morph-2 { + 0%, + 100% { + border-radius: 45% 55% 60% 40% / 55% 45% 50% 50%; + } + 33% { + border-radius: 55% 45% 45% 55% / 45% 55% 55% 45%; + } + 66% { + border-radius: 40% 60% 55% 45% / 50% 50% 45% 55%; + } +} + +@keyframes blob-morph-3 { + 0%, + 100% { + border-radius: 50% 50% 45% 55% / 60% 40% 55% 45%; + } + 50% { + border-radius: 55% 45% 55% 45% / 45% 55% 50% 50%; + } +} + +@keyframes blob-pulse { + 0%, + 100% { + opacity: 0.5; + filter: blur(80px); + } + 50% { + opacity: 0.7; + filter: blur(100px); + } +} + +/* === GLASSMORPHISM UTILITIES === */ +.glass { + background: oklch(0.15 0.02 280 / 0.6); + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border: 1px solid oklch(1 0 0 / 0.1); + box-shadow: + 0 4px 30px oklch(0 0 0 / 0.3), + inset 0 1px 0 oklch(1 0 0 / 0.1); +} + +.glass-neon { + background: oklch(0.12 0.02 280 / 0.65); + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + border: 1px solid oklch(0.7 0.3 340 / 0.3); + box-shadow: + 0 0 20px oklch(0.7 0.3 340 / 0.15), + 0 8px 32px oklch(0 0 0 / 0.3); +} + +/* Light mode fallback */ +:root:not(.dark) .glass, +:root:not(.dark) .glass-neon { + background: oklch(0.98 0.01 90 / 0.7); + border-color: oklch(0.5 0 0 / 0.1); +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .blob { + animation: none; + } +} + +/* Backdrop-filter fallback */ +@supports not (backdrop-filter: blur(20px)) { + .glass, + .glass-neon { + background: oklch(0.12 0.02 280 / 0.95); + } +} + +/* Solana wallet adapter styles - matching outline button style */ +.wallet-adapter-button { + /* Match outline button style */ + background: var(--poline-surface-3) !important; + border: 1px solid var(--poline-accent-5) !important; + color: var(--poline-text-1) !important; + + /* Consistent sizing and typography */ + border-radius: calc(var(--radius, 0.625rem) - 2px) !important; + height: 2.25rem !important; + padding: 0.5rem 1rem !important; + font-size: 0.875rem !important; + font-weight: 500 !important; + box-shadow: none; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + color 0.15s ease, + box-shadow 0.15s ease !important; +} + +.wallet-adapter-button:hover:not([disabled]) { + background: var(--poline-accent-5) !important; + border-color: var(--poline-accent-5) !important; + color: var(--poline-surface-1) !important; + transform: none !important; + box-shadow: none; +} + +.wallet-adapter-button:focus-visible { + outline: none !important; + box-shadow: 0 0 0 3px var(--ring) !important; +} + +.wallet-adapter-button[disabled] { + pointer-events: none !important; + opacity: 0.5 !important; +} + +.wallet-adapter-modal-wrapper { + background: oklch(0.12 0.02 280 / 0.65) !important; + backdrop-filter: blur(24px) saturate(180%) !important; + -webkit-backdrop-filter: blur(24px) saturate(180%) !important; + border: 1px solid oklch(0.7 0.3 340 / 0.3) !important; + box-shadow: + 0 0 20px oklch(0.7 0.3 340 / 0.15), + 0 8px 32px oklch(0 0 0 / 0.3) !important; +} + +.wallet-adapter-modal-title { + color: var(--foreground); +} + +/* Modal overlay/backdrop */ +.wallet-adapter-modal { + background-color: oklch(0.12 0.02 280 / 0.3); +} + +/* Wallet list container */ +.wallet-adapter-modal-list { + margin: 0; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* Individual wallet list items */ +.wallet-adapter-modal-list li { + border-bottom: none; + border-radius: calc(var(--radius, 0.625rem) - 2px); +} + +.wallet-adapter-modal-list li:last-child { + border-bottom: none; +} + +/* Wallet selection buttons */ +.wallet-adapter-modal-list li button { + background-color: transparent !important; + color: var(--foreground) !important; + transition: background-color 0.15s ease; + padding: 1rem 1rem !important; + min-height: 3.5rem !important; +} + +.wallet-adapter-modal-list li button:hover { + background-color: var(--primary) !important; + color: var(--background) !important; +} + +/* Wallet name text */ +.wallet-adapter-modal-list li button span { + color: inherit !important; +} + +/* "Detected" and other status badges */ +.wallet-adapter-modal-list-more { + color: var(--muted-foreground); +} + +/* Close button */ +.wallet-adapter-modal-button-close { + background-color: var(--muted); + color: var(--foreground); +} + +.wallet-adapter-modal-button-close:hover { + background-color: var(--border); +} + +/* Close button SVG icon */ +.wallet-adapter-modal-button-close svg { + fill: var(--foreground); +} + +/* Middle section (collapse/expand) */ +.wallet-adapter-modal-middle { + border-top: 1px solid var(--border); +} + +.wallet-adapter-modal-middle-button { + background-color: transparent; + color: var(--muted-foreground); +} + +.wallet-adapter-modal-middle-button:hover { + color: var(--foreground); +} + +/* === OUTLINE BUTTON OVERRIDES === */ +/* Better contrast with poline palette */ +[data-slot="button"][data-variant="outline"], +button[class*="outline"] { + /* Visible border using accent color */ + border-color: var(--poline-accent-5); + + /* Subtle background for button visibility */ + background: var(--poline-surface-3); + + /* Clear text color */ + color: var(--poline-text-1); +} + +[data-slot="button"][data-variant="outline"]:hover, +button[class*="outline"]:hover { + /* Accent background on hover */ + background: var(--poline-accent-5); + border-color: var(--poline-accent-5); + + /* Dark text for contrast against bright background */ + color: var(--poline-surface-1); +} + +/* === TOAST NOTIFICATIONS === */ +/* Override Sonner's inline CSS variables with !important */ +[data-sonner-toaster] [data-sonner-toast] { + background: oklch(0.12 0.02 280 / 0.65) !important; + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + border: 1px solid oklch(0.7 0.3 340 / 0.3) !important; + box-shadow: + 0 0 20px oklch(0.7 0.3 340 / 0.15), + 0 8px 32px oklch(0 0 0 / 0.3) !important; + color: var(--poline-text-1) !important; +} + +/* Success toast - neon lime accent */ +[data-sonner-toaster] [data-sonner-toast][data-type="success"] { + border-color: oklch(0.7 0.2 145 / 0.4) !important; + box-shadow: + 0 0 20px oklch(0.7 0.2 145 / 0.15), + 0 8px 32px oklch(0 0 0 / 0.3) !important; +} + +/* Error toast - keep visible with appropriate styling */ +[data-sonner-toaster] [data-sonner-toast][data-type="error"] { + border-color: oklch(0.65 0.25 25 / 0.4) !important; + box-shadow: + 0 0 20px oklch(0.65 0.25 25 / 0.15), + 0 8px 32px oklch(0 0 0 / 0.3) !important; +} + +/* === STEP GUIDANCE GLOW EFFECT === */ +@keyframes pink-glow-pulse { + 0%, + 100% { + box-shadow: + 0 0 10px var(--neon-pink), + 0 0 20px oklch(0.7 0.3 340 / 0.5); + } + 50% { + box-shadow: + 0 0 20px var(--neon-pink), + 0 0 40px oklch(0.7 0.3 340 / 0.7), + 0 0 60px oklch(0.7 0.3 340 / 0.4); + } +} + +.glow-pulse { + animation: pink-glow-pulse 2s ease-in-out infinite; +} + +/* Wallet button glow */ +.glow-pulse-container .wallet-adapter-button { + animation: pink-glow-pulse 2s ease-in-out infinite; +} + +.glow-pulse-container .wallet-adapter-button:hover:not([disabled]) { + animation: none; +} + +@media (prefers-reduced-motion: reduce) { + .glow-pulse, + .glow-pulse-container .wallet-adapter-button { + animation: none; + box-shadow: 0 0 15px var(--neon-pink); + } +} + +/* === YOU WIN CELEBRATION ANIMATIONS === */ +@keyframes you-win-overlay-fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes you-win-overlay-fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes you-win-text-scale-in { + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes you-win-text-fade-out { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(1.1); + } +} + +@keyframes you-win-glow-pulse { + 0%, + 100% { + text-shadow: + 0 0 20px var(--neon-pink), + 0 0 40px oklch(0.7 0.3 340 / 0.7), + 0 0 60px oklch(0.7 0.3 340 / 0.5); + } + 50% { + text-shadow: + 0 0 30px var(--neon-pink), + 0 0 60px oklch(0.7 0.3 340 / 0.8), + 0 0 90px oklch(0.7 0.3 340 / 0.6), + 0 0 120px oklch(0.7 0.3 340 / 0.4); + } +} + +.you-win-overlay-fade-in { + animation: you-win-overlay-fade-in 0.3s ease-out forwards; +} + +.you-win-overlay-fade-out { + animation: you-win-overlay-fade-out 0.5s ease-in forwards; +} + +.you-win-text-fade-in { + animation: you-win-text-scale-in 0.4s ease-out forwards; +} + +.you-win-text-fade-out { + animation: you-win-text-fade-out 0.5s ease-in forwards; +} + +.you-win-text { + color: var(--neon-pink); + text-shadow: + 0 0 20px var(--neon-pink), + 0 0 40px oklch(0.7 0.3 340 / 0.7), + 0 0 60px oklch(0.7 0.3 340 / 0.5); +} + +.you-win-glow { + animation: you-win-glow-pulse 0.8s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .you-win-overlay-fade-in, + .you-win-overlay-fade-out, + .you-win-text-fade-in, + .you-win-text-fade-out, + .you-win-glow { + animation: none; + } + .you-win-text { + text-shadow: 0 0 30px var(--neon-pink); + } +} diff --git a/apps/solana/simple-example/app/layout.tsx b/apps/solana/simple-example/app/layout.tsx new file mode 100644 index 0000000..939dc52 --- /dev/null +++ b/apps/solana/simple-example/app/layout.tsx @@ -0,0 +1,57 @@ +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 Solana Kit Simple Example", + description: "Upload blobs to Shelby using your Solana wallet", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {/* Animated background blobs */} +