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
2 changes: 2 additions & 0 deletions apps/aptos/file-upload/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shelby API key - get one from https://docs.shelby.xyz/sdks/typescript/acquire-api-keys
NEXT_PUBLIC_SHELBY_API_KEY=AG-xxx
92 changes: 92 additions & 0 deletions apps/aptos/file-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Aptos File Upload Example

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

## Features

- Connect Aptos wallets via [@aptos-labs/wallet-adapter-react](https://www.npmjs.com/package/@aptos-labs/wallet-adapter-react)
- Upload files to Shelby's decentralized storage
- Native Aptos address as storage account (no derivation needed)
- Clean UI with file selection and upload status

## Prerequisites

- [Node.js](https://nodejs.org/) v22 or higher
- [pnpm](https://pnpm.io/) package manager
- An Aptos wallet (e.g., Petra, Nightly)
- 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/aptos/file-upload
```

### 2. Install Dependencies

From the monorepo root:

```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. Fund Your Account

Testnet tokens (APT and ShelbyUSD) are available through the [Shelby Discord](https://discord.gg/shelbyserves).

### 6. 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 packages:

- [`@shelby-protocol/sdk`](https://docs.shelby.xyz/sdks/typescript) - Core TypeScript SDK for interacting with Shelby Protocol
- [`@shelby-protocol/react`](https://docs.shelby.xyz/sdks/typescript) - React hooks for blob uploads
- [`@aptos-labs/wallet-adapter-react`](https://www.npmjs.com/package/@aptos-labs/wallet-adapter-react) - Aptos wallet connection

### Key Components

- **FileUpload** - Main component handling file selection and upload
- **Header** - Navigation with wallet connection button
- **WalletButton** - Wallet connection with AIP-62 wallet detection
- **providers.tsx** - AptosWalletAdapterProvider and QueryClientProvider configuration

### Storage Account

With Aptos, your wallet address is directly used as your Shelby storage account. No derivation is needed unlike Ethereum or Solana cross-chain flows.

## Learn More

- [Shelby Documentation](https://docs.shelby.xyz)
- [Shelby Explorer](https://explorer.shelby.xyz) - View your uploaded files

## License

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

import { useWallet } from "@aptos-labs/wallet-adapter-react";
import { useUploadBlobs } from "@shelby-protocol/react";
import type { ReactNode } from "react";
import { useCallback, useRef, useState } from "react";
import { shelbyClient } from "../lib/shelby-client";

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

export function FileUpload() {
const { connected, account, signAndSubmitTransaction } = useWallet();
const fileInputRef = useRef<HTMLInputElement>(null);

const storageAddress = account?.address?.toString();

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 || !storageAddress) return;

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

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

await uploadBlobs({
signer: { account: storageAddress, 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/testnet/account/${storageAddress}/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,
storageAddress,
signAndSubmitTransaction,
uploadBlobs,
clearFile,
]);

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

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

if (!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
Aptos 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 || !storageAddress}
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">Storage Account:</span>{" "}
<span className="font-mono">{storageAddress}</span>
</p>
</div>
</section>
);
}
20 changes: 20 additions & 0 deletions apps/aptos/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>
);
}
26 changes: 26 additions & 0 deletions apps/aptos/file-upload/app/components/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { Network } from "@aptos-labs/ts-sdk";
import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export function Providers({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<QueryClientProvider client={queryClient}>
<AptosWalletAdapterProvider
autoConnect
dappConfig={{
network: Network.TESTNET,
}}
>
{children}
</AptosWalletAdapterProvider>
</QueryClientProvider>
);
}
Loading