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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/solana/file-upload/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Solana RPC endpoint (defaults to testnet)
NEXT_PUBLIC_SOLANA_RPC_URL=https://api.testnet.solana.com

# Shelby API key - get one from https://docs.shelby.xyz/sdks/typescript/acquire-api-keys
NEXT_PUBLIC_SHELBY_API_KEY=AG-xxx

95 changes: 95 additions & 0 deletions apps/solana/file-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Solana File Upload Example

A Next.js example application demonstrating how Solana developers can build a file upload dApp on [Shelby Protocol](https://shelby.xyz). This app allows users to connect their Solana wallet and upload files to decentralized storage.

## Features

- Connect Solana wallets via [@solana/react-hooks](https://www.npmjs.com/package/@solana/react-hooks)
- Upload files to Shelby's decentralized storage
- Automatic storage account derivation from Solana address
- Clean UI with drag-and-drop file uploads

## Prerequisites

- [Node.js](https://nodejs.org/) v18 or higher
- [pnpm](https://pnpm.io/) package manager
- A Solana wallet (e.g., Phantom, Solflare)
- A Shelby API key

## Getting Started

### 1. Clone the Repository

```bash
git clone https://github.com/shelby-protocol/shelby-examples.git
cd shelby-examples/apps/solana/file-upload
```

### 2. Install Dependencies

From the monorepo root:

```bash
pnpm install
```

Or from this directory:

```bash
pnpm install
```

### 3. Set Up Environment Variables

Copy the example environment file:

```bash
cp .env.example .env
```

### 4. Get Your Shelby API Key

1. Visit the [Shelby API Keys documentation](https://docs.shelby.xyz/sdks/typescript/acquire-api-keys)
2. Follow the instructions to acquire your API key
3. Add your API key to the `.env` file:

```env
NEXT_PUBLIC_SHELBY_API_KEY=your-api-key-here
```

### 5. Run the Development Server

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser.

## How It Works

This example uses the following Shelby packages:

- [`@shelby-protocol/sdk`](https://docs.shelby.xyz/sdks/typescript) - Core TypeScript SDK for interacting with Shelby Protocol
- [`@shelby-protocol/solana-kit`](https://docs.shelby.xyz/sdks/solana-kit/react) - Solana-specific utilities for wallet integration
- [`@shelby-protocol/react`](https://docs.shelby.xyz/sdks/typescript) - React hooks for blob uploads

### Key Components

- **FileUpload** - Main component handling file selection and upload
- **Header** - Navigation with wallet connection button
- **WalletButton** - Wallet connection using @solana/react-hooks
- **providers.tsx** - SolanaProvider and QueryClientProvider configuration

### Storage Account

When you connect your Solana wallet, a Shelby storage account is automatically derived from your Solana address. This allows you to upload and manage files using your existing wallet.

## Learn More

- [Shelby Documentation](https://docs.shelby.xyz)
- [Solana Kit React Reference](https://docs.shelby.xyz/sdks/solana-kit/react)
- [Shelby Explorer](https://explorer.shelby.xyz) - View your uploaded files

## License

MIT
199 changes: 199 additions & 0 deletions apps/solana/file-upload/app/components/file-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"use client";

import { useUploadBlobs } from "@shelby-protocol/react";
import { useStorageAccount } from "@shelby-protocol/solana-kit/react";
import { useWalletConnection } from "@solana/react-hooks";
import { useCallback, useRef, useState } from "react";
import type { ReactNode } from "react";
import { shelbyClient } from "../lib/shelbyClient";

type UploadStep = "idle" | "uploading" | "done" | "error";

export function FileUpload() {
const { status, wallet } = useWalletConnection();
const fileInputRef = useRef<HTMLInputElement>(null);

// Use the new simplified API - pass wallet directly
const { storageAccountAddress, signAndSubmitTransaction } = useStorageAccount(
{
client: shelbyClient,
wallet,
},
);

const { mutateAsync: uploadBlobs, isPending } = useUploadBlobs({
client: shelbyClient,
});

const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [step, setStep] = useState<UploadStep>("idle");
const [statusMessage, setStatusMessage] = useState<ReactNode | null>(null);

const clearFile = useCallback(() => {
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, []);

const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
setSelectedFile(file);
setStep("idle");
setStatusMessage(null);
},
[],
);

const handleUpload = useCallback(async () => {
if (!selectedFile || !storageAccountAddress) return;

try {
setStep("uploading");
setStatusMessage("Uploading file to Shelby...");

const fileBytes = new Uint8Array(await selectedFile.arrayBuffer());

await uploadBlobs({
signer: { account: storageAccountAddress, signAndSubmitTransaction },
blobs: [
{
blobName: selectedFile.name,
blobData: fileBytes,
},
],
// 30 days from now in microseconds
expirationMicros: (1000 * 60 * 60 * 24 * 30 + Date.now()) * 1000,
});

setStep("done");
const explorerUrl = `https://explorer.shelby.xyz/shelbynet/account/${storageAccountAddress.toString()}/blobs`;
setStatusMessage(
<>
Successfully uploaded: {selectedFile.name}.{" "}
<a
className="underline underline-offset-2"
href={explorerUrl}
target="_blank"
rel="noreferrer"
>
View in Explorer
</a>
</>,
);
clearFile();
} catch (err) {
setStep("error");
const message = err instanceof Error ? err.message : "Unknown error";
const cause =
err instanceof Error && err.cause instanceof Error
? err.cause.message
: undefined;
setStatusMessage(
cause ? `Error: ${message} — ${cause}` : `Error: ${message}`,
);
}
}, [
selectedFile,
storageAccountAddress,
signAndSubmitTransaction,
uploadBlobs,
clearFile,
]);

const handleSelectFile = useCallback(() => {
fileInputRef.current?.click();
}, []);

const isProcessing = step === "uploading" || isPending;

if (status !== "connected") {
return (
<section className="w-full max-w-3xl space-y-4 rounded-2xl border border-border-low bg-card p-6 shadow-[0_20px_80px_-50px_rgba(0,0,0,0.35)]">
<div className="space-y-1">
<p className="text-lg font-semibold">Upload File to Shelby</p>
<p className="text-sm text-muted">
Connect your wallet to upload files to decentralized storage.
</p>
</div>
<div className="rounded-lg bg-cream/50 p-4 text-center text-sm text-muted">
Wallet not connected
</div>
</section>
);
}

return (
<section className="w-full max-w-3xl space-y-4 rounded-2xl border border-border-low bg-card p-6 shadow-[0_20px_80px_-50px_rgba(0,0,0,0.35)]">
<div className="space-y-1">
<p className="text-lg font-semibold">Upload File to Shelby</p>
<p className="text-sm text-muted">
Upload any file to Shelby&apos;s decentralized storage using your
Solana wallet.
</p>
</div>

{/* File Input */}
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
disabled={isProcessing}
className="hidden"
/>

<div
onClick={handleSelectFile}
className="cursor-pointer rounded-xl border-2 border-dashed border-border-low bg-cream/30 p-8 text-center transition hover:border-foreground/30 hover:bg-cream/50"
>
{selectedFile ? (
<div className="space-y-1">
<p className="font-medium">{selectedFile.name}</p>
<p className="text-sm text-muted">
{(selectedFile.size / 1024).toFixed(1)} KB
</p>
</div>
) : (
<div className="space-y-1">
<p className="text-sm text-muted">Click to select a file</p>
</div>
)}
</div>

{/* Upload Button */}
<button
onClick={handleUpload}
disabled={isProcessing || !selectedFile || !storageAccountAddress}
className="w-full rounded-lg bg-foreground px-5 py-2.5 text-sm font-medium text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
{isProcessing ? "Uploading..." : "Upload to Shelby"}
</button>
</div>

{/* Status Message */}
{statusMessage && (
<div
className={`rounded-lg border px-4 py-3 text-sm break-all ${
step === "error"
? "border-red-300 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
: step === "done"
? "border-green-300 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-900/20 dark:text-green-400"
: "border-border-low bg-cream/50"
}`}
>
{statusMessage}
</div>
)}

{/* Storage Account Info */}
<div className="border-t border-border-low pt-4 text-xs text-muted">
<p>
<span className="font-medium">Shelby Storage Account:</span>{" "}
<span className="font-mono">{storageAccountAddress?.toString()}</span>
</p>
</div>
</section>
);
}
20 changes: 20 additions & 0 deletions apps/solana/file-upload/app/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import Link from "next/link";
import { WalletButton } from "./wallet-button";

export function Header() {
return (
<header className="sticky top-0 z-30 w-full border-b border-border-low bg-background/80 backdrop-blur">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4 sm:px-6">
<Link href="/" className="flex items-center gap-2 font-semibold">
<span className="h-8 w-8 rounded-lg bg-foreground text-background grid place-items-center text-sm">
FU
</span>
<span className="tracking-tight">File Upload</span>
</Link>
<WalletButton />
</div>
</header>
);
}
32 changes: 32 additions & 0 deletions apps/solana/file-upload/app/components/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { autoDiscover, createClient } from "@solana/client";
import { SolanaProvider } from "@solana/react-hooks";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "../lib/config";

// Filter to only show wallets that support Solana features
const isSolanaWallet = (wallet: { features: Record<string, unknown> }) => {
return Object.keys(wallet.features).some((feature) =>
feature.startsWith("solana:"),
);
};

const client = createClient({
endpoint: config.solanaRpcUrl,
walletConnectors: autoDiscover({ filter: isSolanaWallet }),
});

const queryClient = new QueryClient();

export function Providers({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<QueryClientProvider client={queryClient}>
<SolanaProvider client={client}>{children}</SolanaProvider>
</QueryClientProvider>
);
}
Loading