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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,71 @@ const connection = new web3.Connection("{rpc-url}", "confirmed");
const drive = await new ShdwDrive(connection, wallet).init();
```

## Authentication

Shadow Drive uses **Solana wallet signatures** for authentication. You only need a Solana wallet - no additional API keys or Shadow Drive accounts required.

### RPC Endpoint Configuration

You can use any Solana RPC endpoint. The example code shows GenesysGo's RPC service, but this is optional:

**Option 1: Public Solana RPC (No additional credentials needed)**
```js
const connection = new web3.Connection("https://api.mainnet-beta.solana.com");
```

**Option 2: GenesysGo RPC (Requires their RPC credentials)**
```js
const connection = new web3.Connection(
"https://us-west-1.genesysgo.net/{YOUR_RPC_ACCOUNT_ID}",
{
commitment: "confirmed",
httpHeaders: {
Authorization: "Bearer {YOUR_RPC_ACCESS_TOKEN}",
},
}
);
```

**Option 3: Other RPC providers**
```js
// Helius, QuickNode, Alchemy, etc.
const connection = new web3.Connection("{YOUR_PREFERRED_RPC_URL}");
```

### Shadow Drive Setup

Regardless of your RPC choice, Shadow Drive setup is the same:

```js
// Your Solana wallet (browser wallet, keypair, etc.)
const wallet = /* your wallet instance */;

// Initialize Shadow Drive
const drive = await new ShdwDrive(connection, wallet).init();
```

### Understanding the Example Placeholders

In `examples/web/src/App.tsx`, the placeholders refer to **GenesysGo's RPC service credentials**:

- `{YOUR_ACCOUNT_UUID_HERE}` = Your GenesysGo RPC account identifier
- `{GENESYSGO AUTHENTICATION TOKEN HERE}` = Your GenesysGo RPC access token

**To use the example:**
1. **Option A**: Replace with your GenesysGo RPC credentials
2. **Option B**: Change to a public RPC endpoint and remove the Authorization header

### How Shadow Drive Authentication Works

1. **RPC Authentication**: Handled by your connection configuration (varies by provider)
2. **Shadow Drive Authentication**: Automatic via wallet signatures
- No API keys needed
- Uses cryptographic message signing
- Wallet signs authentication messages for each operation

> **Note**: GenesysGo RPC credentials are for blockchain access only, not Shadow Drive storage. Shadow Drive authenticates through your Solana wallet automatically.

### Examples

| package | description |
Expand Down
29 changes: 28 additions & 1 deletion examples/web/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
# React Shadow Drive Example

## Prerequisites

Before running this example, you'll need:

1. **Solana Wallet**: Any Solana-compatible wallet (Phantom, Solflare, etc.)

2. **RPC Endpoint Configuration**: Choose one option for `src/App.tsx`:

**Option A: Use Public RPC** (No additional setup required)
```tsx
// Line 28: Replace with public RPC
const network = "https://api.mainnet-beta.solana.com";

// Lines 42-48: Remove the httpHeaders config entirely
<ConnectionProvider endpoint={network} config={{ commitment: "confirmed" }}>
```

**Option B: Use GenesysGo RPC** (Requires GenesysGo RPC credentials)
```tsx
// Line 28: Replace with your GenesysGo RPC account ID
const network = "https://us-west-1.genesysgo.net/YOUR_RPC_ACCOUNT_ID";

// Line 46: Replace with your GenesysGo RPC access token
Authorization: "Bearer YOUR_RPC_ACCESS_TOKEN",
```

> **Note**: The placeholders in App.tsx are for **RPC access**, not Shadow Drive. Shadow Drive authentication is handled automatically through your connected wallet.

## Getting Started

- From the root directory of this project:


```bash
yarn install
cd examples/web
Expand Down
91 changes: 90 additions & 1 deletion examples/web/src/Drive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import * as anchor from "@project-serum/anchor";
import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
import { PublicKey } from "@solana/web3.js";
import { CircularProgress, TextField, FormControl, Select, InputLabel, MenuItem, Button, FormLabel, RadioGroup, FormControlLabel, Radio, styled, LinearProgress, Container, Grid } from "@mui/material";
import { CircularProgress, TextField, FormControl, Select, InputLabel, MenuItem, Button, FormLabel, RadioGroup, FormControlLabel, Radio, styled, LinearProgress, Container, Grid, Chip } from "@mui/material";

const bytesToHuman = (bytes: any, si = false, dp = 1) => {
const thresh = si ? 1024 : 1024;
Expand All @@ -29,6 +29,39 @@ const bytesToHuman = (bytes: any, si = false, dp = 1) => {

return bytes.toFixed(dp) + " " + units[u];
}

// SHDW token mint address
const SHDW_TOKEN_MINT = new PublicKey("SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y");

// Helper function to find associated token account
async function findAssociatedTokenAddress(
walletAddress: PublicKey,
tokenMintAddress: PublicKey
): Promise<PublicKey> {
const [address] = await PublicKey.findProgramAddress(
[
walletAddress.toBuffer(),
new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").toBuffer(),
tokenMintAddress.toBuffer(),
],
new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
);
return address;
}

// Calculate maximum storage size based on SHDW balance
// Rough estimate: 1 SHDW ≈ 1GB storage per year
function calculateMaxStorageFromBalance(shdwBalance: number): string {
if (shdwBalance < 0.1) return "1GB"; // Default to minimum

const maxGB = Math.floor(shdwBalance * 0.9); // Use 90% of balance for safety

if (maxGB >= 50) return "50GB"; // Max option available
if (maxGB >= 10) return "10GB";
if (maxGB >= 1) return "1GB";

return "1GB"; // Minimum option
}
/**
*
* Simple usage examples for Shadow Drive
Expand All @@ -51,6 +84,8 @@ export default function Drive() {
const [loading, setLoading] = useState<boolean>();
const [tx, setTx] = useState<String>();
const [version, setVersion] = useState<ShadowDriveVersion>('v2');
const [shdwBalance, setShdwBalance] = useState<number>(0);
const [balanceLoading, setBalanceLoading] = useState<boolean>(false);
const submitForm = async () => {
if (!acc?.publicKey || !fileList) return;
try {
Expand Down Expand Up @@ -100,12 +135,37 @@ export default function Drive() {
setLoading(true);
const result = await drive?.createStorageAccount(accName, accSize, version);
setTx(result!.transaction_signature);
// Refresh balance after creating account
await getUserShdwBalance();
} catch (e) {
console.log(e);
}
refreshAccounts();
setLoading(false);
}

// Get user's SHDW token balance
const getUserShdwBalance = async () => {
if (!wallet.publicKey || !connection) return;

try {
setBalanceLoading(true);
const userATA = await findAssociatedTokenAddress(wallet.publicKey, SHDW_TOKEN_MINT);
const balance = await connection.getTokenAccountBalance(userATA);
setShdwBalance(balance.value.uiAmount || 0);
} catch (e) {
console.log("Error fetching SHDW balance:", e);
setShdwBalance(0);
} finally {
setBalanceLoading(false);
}
};

// Handle "Max Stake" button click
const handleMaxStake = () => {
const maxSize = calculateMaxStorageFromBalance(shdwBalance);
setAccSize(maxSize);
};
useEffect(() => {
(async () => {
if (wallet) {
Expand All @@ -121,8 +181,15 @@ export default function Drive() {
useEffect(() => {
if (drive) {
refreshAccounts();
getUserShdwBalance();
}
}, [drive])

useEffect(() => {
if (wallet.connected && connection) {
getUserShdwBalance();
}
}, [wallet.connected])
useEffect(() => {
console.log('uploaded');
if (displayFiles) {
Expand All @@ -146,6 +213,16 @@ export default function Drive() {

<div style={{ marginTop: '50px', maxWidth: '500px' }}>
<h2 style={{ marginBottom: '20px' }}>Create a Shadow Drive account:</h2>

{/* SHDW Balance Display */}
<div style={{ marginBottom: '20px' }}>
<Chip
label={balanceLoading ? "Loading balance..." : `SHDW Balance: ${shdwBalance.toFixed(2)}`}
color={shdwBalance > 0 ? "success" : "default"}
variant="outlined"
size="medium"
/>
</div>
<form>
<TextField color="secondary" type="text" name="storageAccount" label="Storage Name" variant="standard"
focused
Expand Down Expand Up @@ -176,6 +253,18 @@ export default function Drive() {
<MenuItem value={'50GB'}>50GB</MenuItem>
</Select>
</FormControl>

{/* Max Stake Button */}
<Button
size="small"
variant="outlined"
color="secondary"
sx={{ marginLeft: '10px', height: '32px' }}
onClick={handleMaxStake}
disabled={!shdwBalance || balanceLoading}
>
Max Stake
</Button>
<FormControl sx={{ marginLeft: '20px', width: '100px' }}
focused>
<InputLabel id="version-select"
Expand Down