@blu1606/create-walrus-app 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator/file-ops.d.ts +8 -0
- package/dist/generator/file-ops.js +20 -0
- package/dist/generator/index.js +37 -22
- package/dist/generator/layers.d.ts +15 -2
- package/dist/generator/layers.js +38 -47
- package/dist/generator/types.d.ts +9 -1
- package/dist/index.js +1 -2
- package/dist/post-install/git.d.ts +8 -0
- package/dist/post-install/git.js +2 -0
- package/dist/post-install/index.d.ts +0 -1
- package/dist/post-install/index.js +2 -15
- package/dist/post-install/messages.js +1 -1
- package/package.json +3 -3
- package/{templates/base → presets/react-mysten-gallery}/.env.example +31 -31
- package/presets/react-mysten-gallery/.gitkeep +4 -0
- package/{templates/gallery → presets/react-mysten-gallery}/README.md +25 -22
- package/presets/react-mysten-gallery/package.json +34 -0
- package/presets/react-mysten-gallery/src/App.tsx +23 -0
- package/presets/react-mysten-gallery/src/components/features/file-card.tsx +89 -0
- package/{templates/gallery/src/components/GalleryGrid.tsx → presets/react-mysten-gallery/src/components/features/gallery-grid.tsx} +5 -5
- package/presets/react-mysten-gallery/src/components/features/upload-modal.tsx +69 -0
- package/{templates/react/src/components/WalletConnect.tsx → presets/react-mysten-gallery/src/components/features/wallet-connect.tsx} +1 -1
- package/presets/react-mysten-gallery/src/components/layout/app-layout.tsx +21 -0
- package/{templates/react/src/hooks/useStorage.ts → presets/react-mysten-gallery/src/hooks/use-download.ts} +2 -18
- package/presets/react-mysten-gallery/src/hooks/use-upload.ts +49 -0
- package/{templates/react/src/hooks/useWallet.ts → presets/react-mysten-gallery/src/hooks/use-wallet.ts} +2 -7
- package/presets/react-mysten-gallery/src/index.css +384 -0
- package/presets/react-mysten-gallery/src/index.ts +17 -0
- package/presets/react-mysten-gallery/src/lib/walrus/adapter.ts +197 -0
- package/presets/react-mysten-gallery/src/lib/walrus/client.ts +87 -0
- package/presets/react-mysten-gallery/src/lib/walrus/index.ts +4 -0
- package/presets/react-mysten-gallery/src/lib/walrus/types.ts +101 -0
- package/{templates/react → presets/react-mysten-gallery}/src/main.tsx +0 -1
- package/{templates/react → presets/react-mysten-gallery}/src/providers/WalletProvider.tsx +16 -1
- package/{templates/base → presets/react-mysten-gallery}/src/utils/env.ts +41 -41
- package/{templates/gallery → presets/react-mysten-gallery}/src/utils/index-manager.ts +2 -2
- package/presets/react-mysten-gallery/src/utils/mime-type.ts +97 -0
- package/presets/react-mysten-gallery/src/utils/preview-generator.ts +134 -0
- package/{templates/react → presets/react-mysten-gallery}/tsconfig.json +20 -8
- package/presets/react-mysten-simple-upload/.env.example +31 -0
- package/presets/react-mysten-simple-upload/.gitkeep +4 -0
- package/presets/react-mysten-simple-upload/index.html +13 -0
- package/{templates/react → presets/react-mysten-simple-upload}/package.json +13 -11
- package/presets/react-mysten-simple-upload/src/App.tsx +27 -0
- package/presets/react-mysten-simple-upload/src/components/features/file-preview.tsx +73 -0
- package/{templates/simple-upload/src/components/UploadForm.tsx → presets/react-mysten-simple-upload/src/components/features/upload-form.tsx} +15 -5
- package/presets/react-mysten-simple-upload/src/components/features/wallet-connect.tsx +21 -0
- package/presets/react-mysten-simple-upload/src/components/layout/app-layout.tsx +21 -0
- package/presets/react-mysten-simple-upload/src/hooks/use-download.ts +24 -0
- package/presets/react-mysten-simple-upload/src/hooks/use-upload.ts +49 -0
- package/presets/react-mysten-simple-upload/src/hooks/use-wallet.ts +11 -0
- package/presets/react-mysten-simple-upload/src/index.css +252 -0
- package/presets/react-mysten-simple-upload/src/index.ts +16 -0
- package/presets/react-mysten-simple-upload/src/lib/walrus/adapter.ts +197 -0
- package/presets/react-mysten-simple-upload/src/lib/walrus/client.ts +87 -0
- package/presets/react-mysten-simple-upload/src/lib/walrus/index.ts +4 -0
- package/{templates/base/src/adapters/storage.ts → presets/react-mysten-simple-upload/src/lib/walrus/types.ts} +83 -58
- package/presets/react-mysten-simple-upload/src/main.tsx +16 -0
- package/presets/react-mysten-simple-upload/src/providers/QueryProvider.tsx +18 -0
- package/presets/react-mysten-simple-upload/src/providers/WalletProvider.tsx +52 -0
- package/presets/react-mysten-simple-upload/src/utils/env.ts +41 -0
- package/presets/react-mysten-simple-upload/src/utils/mime-type.ts +97 -0
- package/presets/react-mysten-simple-upload/tsconfig.json +39 -0
- package/presets/react-mysten-simple-upload/tsconfig.node.json +10 -0
- package/presets/react-mysten-simple-upload/vite.config.ts +19 -0
- package/templates/base/README.md +0 -54
- package/templates/base/package.json +0 -19
- package/templates/base/src/types/index.ts +0 -9
- package/templates/base/src/types/walrus.ts +0 -22
- package/templates/base/src/utils/format.ts +0 -29
- package/templates/base/tsconfig.json +0 -19
- package/templates/gallery/package.json +0 -6
- package/templates/gallery/src/App.tsx +0 -21
- package/templates/gallery/src/components/FileCard.tsx +0 -27
- package/templates/gallery/src/components/UploadModal.tsx +0 -45
- package/templates/gallery/src/styles.css +0 -58
- package/templates/gallery/src/types/gallery.ts +0 -13
- package/templates/react/.eslintrc.json +0 -26
- package/templates/react/README.md +0 -80
- package/templates/react/src/App.tsx +0 -14
- package/templates/react/src/components/Layout.tsx +0 -21
- package/templates/react/src/dapp-kit.css +0 -1
- package/templates/react/src/index.css +0 -50
- package/templates/react/src/index.ts +0 -10
- package/templates/sdk-mysten/README.md +0 -65
- package/templates/sdk-mysten/package.json +0 -14
- package/templates/sdk-mysten/src/adapter.ts +0 -80
- package/templates/sdk-mysten/src/client.ts +0 -45
- package/templates/sdk-mysten/src/config.ts +0 -33
- package/templates/sdk-mysten/src/index.ts +0 -11
- package/templates/sdk-mysten/src/types.ts +0 -19
- package/templates/sdk-mysten/test/adapter.test.ts +0 -20
- package/templates/simple-upload/package.json +0 -6
- package/templates/simple-upload/src/App.tsx +0 -27
- package/templates/simple-upload/src/components/FilePreview.tsx +0 -40
- package/templates/simple-upload/src/styles.css +0 -33
- /package/{templates/react → presets/react-mysten-gallery}/index.html +0 -0
- /package/{templates/react → presets/react-mysten-gallery}/src/providers/QueryProvider.tsx +0 -0
- /package/{templates/react → presets/react-mysten-gallery}/tsconfig.node.json +0 -0
- /package/{templates/react → presets/react-mysten-gallery}/vite.config.ts +0 -0
- /package/{templates/simple-upload → presets/react-mysten-simple-upload}/README.md +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { storageAdapter } from '../lib/walrus/adapter.js';
|
|
2
|
+
import { loadEnv } from './env.js';
|
|
3
|
+
|
|
4
|
+
// Walrus aggregator URLs for direct HTTP access
|
|
5
|
+
const AGGREGATOR_URLS = {
|
|
6
|
+
testnet: 'https://aggregator.walrus-testnet.walrus.space',
|
|
7
|
+
mainnet: 'https://aggregator.walrus.space',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the aggregator URL based on current network configuration
|
|
12
|
+
*/
|
|
13
|
+
function getAggregatorUrl(): string {
|
|
14
|
+
const env = loadEnv();
|
|
15
|
+
// Use environment override if available
|
|
16
|
+
if (env.walrusAggregator) {
|
|
17
|
+
return env.walrusAggregator;
|
|
18
|
+
}
|
|
19
|
+
// Default to testnet
|
|
20
|
+
const network = env.walrusNetwork === 'mainnet' ? 'mainnet' : 'testnet';
|
|
21
|
+
return AGGREGATOR_URLS[network];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a preview URL by fetching directly from Walrus aggregator HTTP API
|
|
26
|
+
* This is faster than using the SDK as it makes a direct HTTP request
|
|
27
|
+
*
|
|
28
|
+
* @param blobId - The Walrus blob ID
|
|
29
|
+
* @param contentType - MIME type of the content (e.g., 'image/jpeg')
|
|
30
|
+
* @returns Object URL that can be used in <img> src
|
|
31
|
+
*/
|
|
32
|
+
async function generatePreviewUrlDirect(
|
|
33
|
+
blobId: string,
|
|
34
|
+
contentType: string
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const aggregatorUrl = getAggregatorUrl();
|
|
37
|
+
|
|
38
|
+
// Fetch blob data directly from aggregator
|
|
39
|
+
// Using the /v1/blobs/{blobId} endpoint which returns the full blob
|
|
40
|
+
const response = await fetch(`${aggregatorUrl}/v1/blobs/${blobId}`, {
|
|
41
|
+
headers: {
|
|
42
|
+
'Accept': 'application/octet-stream',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get blob data and create object URL
|
|
51
|
+
const blobData = await response.blob();
|
|
52
|
+
|
|
53
|
+
// Create a new blob with correct content type
|
|
54
|
+
const typedBlob = new Blob([blobData], { type: contentType });
|
|
55
|
+
const objectUrl = URL.createObjectURL(typedBlob);
|
|
56
|
+
|
|
57
|
+
return objectUrl;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fallback: Generate preview using SDK download method
|
|
62
|
+
*/
|
|
63
|
+
async function generatePreviewUrlFallback(
|
|
64
|
+
blobId: string,
|
|
65
|
+
contentType: string
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
// Download blob data as bytes via SDK
|
|
68
|
+
const bytes = await storageAdapter.download(blobId);
|
|
69
|
+
|
|
70
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
71
|
+
throw new Error('Expected Uint8Array from download');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create a Blob from the bytes with correct content type
|
|
75
|
+
const buffer = new ArrayBuffer(bytes.length);
|
|
76
|
+
const view = new Uint8Array(buffer);
|
|
77
|
+
view.set(bytes);
|
|
78
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
79
|
+
|
|
80
|
+
// Create and return object URL
|
|
81
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
82
|
+
|
|
83
|
+
return objectUrl;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate a preview URL from blob ID
|
|
88
|
+
* Tries direct HTTP fetch first (faster), falls back to SDK if it fails
|
|
89
|
+
*
|
|
90
|
+
* @param blobId - The Walrus blob ID
|
|
91
|
+
* @param contentType - MIME type of the content (e.g., 'image/jpeg')
|
|
92
|
+
* @returns Object URL that can be used in <img> src
|
|
93
|
+
*/
|
|
94
|
+
export async function generatePreviewUrl(
|
|
95
|
+
blobId: string,
|
|
96
|
+
contentType: string
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
try {
|
|
99
|
+
// Try direct HTTP fetch first (faster)
|
|
100
|
+
console.log(`[Preview] Attempting direct fetch for ${blobId.slice(0, 16)}...`);
|
|
101
|
+
const url = await generatePreviewUrlDirect(blobId, contentType);
|
|
102
|
+
console.log(`[Preview] Direct fetch successful`);
|
|
103
|
+
return url;
|
|
104
|
+
} catch (directError) {
|
|
105
|
+
console.warn(`[Preview] Direct fetch failed, falling back to SDK:`, directError);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Fallback to SDK method
|
|
109
|
+
const url = await generatePreviewUrlFallback(blobId, contentType);
|
|
110
|
+
console.log(`[Preview] SDK fallback successful`);
|
|
111
|
+
return url;
|
|
112
|
+
} catch (fallbackError) {
|
|
113
|
+
console.error(`[Preview] Both methods failed for ${blobId}:`, fallbackError);
|
|
114
|
+
throw fallbackError;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if content type is an image that can be displayed
|
|
121
|
+
*/
|
|
122
|
+
export function isImageType(contentType: string): boolean {
|
|
123
|
+
return contentType.startsWith('image/');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Revoke object URL to free memory
|
|
128
|
+
* Call this when the component unmounts or the URL is no longer needed
|
|
129
|
+
*/
|
|
130
|
+
export function revokePreviewUrl(url: string): void {
|
|
131
|
+
if (url.startsWith('blob:')) {
|
|
132
|
+
URL.revokeObjectURL(url);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -2,26 +2,38 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
|
-
"lib": [
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
6
10
|
"module": "ESNext",
|
|
7
11
|
"skipLibCheck": true,
|
|
8
|
-
|
|
9
12
|
"moduleResolution": "bundler",
|
|
10
13
|
"allowImportingTsExtensions": true,
|
|
11
14
|
"resolveJsonModule": true,
|
|
12
15
|
"isolatedModules": true,
|
|
13
16
|
"noEmit": true,
|
|
14
17
|
"jsx": "react-jsx",
|
|
15
|
-
|
|
18
|
+
"types": [
|
|
19
|
+
"vite/client"
|
|
20
|
+
],
|
|
16
21
|
"strict": true,
|
|
17
22
|
"noUnusedLocals": true,
|
|
18
23
|
"noUnusedParameters": true,
|
|
19
24
|
"noFallthroughCasesInSwitch": true,
|
|
20
|
-
|
|
21
25
|
"paths": {
|
|
22
|
-
"@/*": [
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./src/*"
|
|
28
|
+
]
|
|
23
29
|
}
|
|
24
30
|
},
|
|
25
|
-
"include": [
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
"include": [
|
|
32
|
+
"src"
|
|
33
|
+
],
|
|
34
|
+
"references": [
|
|
35
|
+
{
|
|
36
|
+
"path": "./tsconfig.node.json"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## ==============================================
|
|
2
|
+
## Walrus Application - Environment Configuration
|
|
3
|
+
## ==============================================
|
|
4
|
+
|
|
5
|
+
## WALRUS NETWORK SETTINGS
|
|
6
|
+
## Network: testnet | mainnet | devnet
|
|
7
|
+
VITE_WALRUS_NETWORK=testnet
|
|
8
|
+
|
|
9
|
+
## Walrus Aggregator URL (for downloads)
|
|
10
|
+
VITE_WALRUS_AGGREGATOR=https://aggregator.walrus-testnet.walrus.space
|
|
11
|
+
|
|
12
|
+
## Walrus Publisher URL (for uploads)
|
|
13
|
+
VITE_WALRUS_PUBLISHER=https://publisher.walrus-testnet.walrus.space
|
|
14
|
+
|
|
15
|
+
## SUI BLOCKCHAIN SETTINGS
|
|
16
|
+
## Sui Network: testnet | mainnet | devnet
|
|
17
|
+
VITE_SUI_NETWORK=testnet
|
|
18
|
+
|
|
19
|
+
## Sui RPC URL (for wallet interactions)
|
|
20
|
+
VITE_SUI_RPC=https://fullnode.testnet.sui.io:443
|
|
21
|
+
|
|
22
|
+
## OPTIONAL FEATURES
|
|
23
|
+
## Blockberry Analytics API Key (leave empty to disable)
|
|
24
|
+
VITE_BLOCKBERRY_KEY=
|
|
25
|
+
|
|
26
|
+
## ==============================================
|
|
27
|
+
## PREREQUISITES
|
|
28
|
+
## ==============================================
|
|
29
|
+
## 1. Install Sui Wallet browser extension
|
|
30
|
+
## 2. Get testnet SUI from faucet: https://faucet.testnet.sui.io/
|
|
31
|
+
## 3. Copy this file to .env and fill in any optional values
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Walrus App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -2,31 +2,33 @@
|
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
|
+
"description": "Walrus application scaffolded with create-walrus-app",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"dev": "vite",
|
|
8
8
|
"build": "tsc && vite build",
|
|
9
|
-
"
|
|
9
|
+
"dev": "vite",
|
|
10
10
|
"lint": "eslint . --ext .ts,.tsx",
|
|
11
|
+
"preview": "vite preview",
|
|
11
12
|
"type-check": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"react": "^18.2.0",
|
|
15
|
-
"react-dom": "^18.2.0",
|
|
16
|
-
"@tanstack/react-query": "^5.17.0",
|
|
17
15
|
"@mysten/dapp-kit": "^0.14.0",
|
|
18
|
-
"@mysten/sui": "^1.10.0"
|
|
16
|
+
"@mysten/sui": "^1.10.0",
|
|
17
|
+
"@mysten/walrus": "^0.9.0",
|
|
18
|
+
"@tanstack/react-query": "^5.17.0",
|
|
19
|
+
"react": "^18.2.0",
|
|
20
|
+
"react-dom": "^18.2.0"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
21
23
|
"@types/react": "^18.2.48",
|
|
22
24
|
"@types/react-dom": "^18.2.18",
|
|
23
|
-
"@vitejs/plugin-react": "^4.2.1",
|
|
24
|
-
"vite": "^5.0.11",
|
|
25
|
-
"typescript": "^5.3.3",
|
|
26
|
-
"eslint": "^8.56.0",
|
|
27
25
|
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
|
28
26
|
"@typescript-eslint/parser": "^6.19.0",
|
|
27
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
28
|
+
"eslint": "^8.56.0",
|
|
29
29
|
"eslint-plugin-react": "^7.33.2",
|
|
30
|
-
"eslint-plugin-react-hooks": "^4.6.0"
|
|
30
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
31
|
+
"typescript": "^5.3.3",
|
|
32
|
+
"vite": "^5.0.11"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AppLayout } from './components/layout/app-layout.js';
|
|
2
|
+
import { UploadForm } from './components/features/upload-form.js';
|
|
3
|
+
import { FilePreview } from './components/features/file-preview.js';
|
|
4
|
+
import './index.css';
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
return (
|
|
8
|
+
<AppLayout>
|
|
9
|
+
<div className="simple-upload-app">
|
|
10
|
+
<h2><span className="text-accent">📤</span> Simple Upload</h2>
|
|
11
|
+
<p className="text-secondary">Upload a file to <span className="text-accent">Walrus</span> and download it by <span className="text-accent">Blob ID</span></p>
|
|
12
|
+
|
|
13
|
+
<section className="upload-section">
|
|
14
|
+
<h3>Upload File</h3>
|
|
15
|
+
<UploadForm />
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section className="download-section">
|
|
19
|
+
<h3>Download File</h3>
|
|
20
|
+
<FilePreview />
|
|
21
|
+
</section>
|
|
22
|
+
</div>
|
|
23
|
+
</AppLayout>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default App;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useDownload, useMetadata } from '../../hooks/use-download.js';
|
|
3
|
+
|
|
4
|
+
export function FilePreview() {
|
|
5
|
+
const [blobId, setBlobId] = useState('');
|
|
6
|
+
const { data, isLoading, error } = useDownload(blobId);
|
|
7
|
+
const { data: metadata } = useMetadata(blobId);
|
|
8
|
+
|
|
9
|
+
const handleDownload = () => {
|
|
10
|
+
if (!data) return;
|
|
11
|
+
|
|
12
|
+
// Use original filename from metadata if available
|
|
13
|
+
let filename = metadata?.fileName || `walrus-${blobId.slice(0, 8)}`;
|
|
14
|
+
|
|
15
|
+
// If metadata doesn't have filename, auto-detect from content type
|
|
16
|
+
if (!metadata?.fileName) {
|
|
17
|
+
const contentType = metadata?.contentType || 'application/octet-stream';
|
|
18
|
+
|
|
19
|
+
if (contentType.startsWith('image/')) {
|
|
20
|
+
filename += `.${contentType.split('/')[1]}`;
|
|
21
|
+
} else if (contentType.startsWith('text/')) {
|
|
22
|
+
filename += '.txt';
|
|
23
|
+
} else if (contentType.includes('json')) {
|
|
24
|
+
filename += '.json';
|
|
25
|
+
} else {
|
|
26
|
+
filename += '.bin';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const contentType = metadata?.contentType || 'application/octet-stream';
|
|
31
|
+
const blob = new Blob([data as Uint8Array], { type: contentType });
|
|
32
|
+
const url = URL.createObjectURL(blob);
|
|
33
|
+
const a = document.createElement('a');
|
|
34
|
+
a.href = url;
|
|
35
|
+
a.download = filename;
|
|
36
|
+
a.click();
|
|
37
|
+
URL.revokeObjectURL(url);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Auto-detect if data is text or JSON for preview
|
|
41
|
+
const canPreview = data && (typeof data === 'string' || data instanceof Uint8Array);
|
|
42
|
+
const preview = canPreview && typeof data === 'string'
|
|
43
|
+
? data.slice(0, 200) // Show first 200 chars for text
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="file-preview">
|
|
48
|
+
<input
|
|
49
|
+
type="text"
|
|
50
|
+
placeholder="Enter Blob ID"
|
|
51
|
+
value={blobId}
|
|
52
|
+
onChange={(e) => setBlobId(e.target.value)}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{isLoading && <p className="text-secondary">Loading...</p>}
|
|
56
|
+
{error && <p className="error">Error: {error.message}</p>}
|
|
57
|
+
|
|
58
|
+
{data && (
|
|
59
|
+
<div className="preview-content icon-list">
|
|
60
|
+
<p className="text-success">✓ Blob found <span className="text-secondary">({data.byteLength || data.length} bytes)</span></p>
|
|
61
|
+
{metadata?.fileName && <p className="text-secondary">File: <span className="text-accent">{metadata.fileName}</span></p>}
|
|
62
|
+
{metadata?.contentType && <p className="text-secondary">Type: <span className="text-accent">{metadata.contentType}</span></p>}
|
|
63
|
+
{preview && (
|
|
64
|
+
<pre>
|
|
65
|
+
{preview}...
|
|
66
|
+
</pre>
|
|
67
|
+
)}
|
|
68
|
+
<button onClick={handleDownload}>Download File</button>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { useUpload } from '
|
|
2
|
+
import { useUpload } from '../../hooks/use-upload.js';
|
|
3
|
+
import { useWallet } from '../../hooks/use-wallet.js';
|
|
3
4
|
|
|
4
5
|
export function UploadForm() {
|
|
5
6
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
6
7
|
const upload = useUpload();
|
|
8
|
+
const { isConnected } = useWallet();
|
|
7
9
|
|
|
8
10
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
9
11
|
const file = e.target.files?.[0];
|
|
@@ -33,16 +35,24 @@ export function UploadForm() {
|
|
|
33
35
|
|
|
34
36
|
{selectedFile && (
|
|
35
37
|
<div className="file-info">
|
|
36
|
-
<p>Selected: {selectedFile.name}</p>
|
|
37
|
-
<p>Size: {(selectedFile.size / 1024).toFixed(2)} KB</p>
|
|
38
|
+
<p className="text-secondary">Selected: <span className="text-accent">{selectedFile.name}</span></p>
|
|
39
|
+
<p className="text-secondary">Size: <span className="text-accent">{(selectedFile.size / 1024).toFixed(2)} KB</span></p>
|
|
38
40
|
</div>
|
|
39
41
|
)}
|
|
40
42
|
|
|
43
|
+
{!isConnected && (
|
|
44
|
+
<p className="warning">⚠️ Please connect your wallet to upload files</p>
|
|
45
|
+
)}
|
|
46
|
+
|
|
41
47
|
<button
|
|
42
48
|
onClick={handleUpload}
|
|
43
|
-
disabled={!selectedFile || upload.isPending}
|
|
49
|
+
disabled={!selectedFile || !isConnected || upload.isPending}
|
|
44
50
|
>
|
|
45
|
-
{
|
|
51
|
+
{!isConnected
|
|
52
|
+
? 'Connect Wallet First'
|
|
53
|
+
: upload.isPending
|
|
54
|
+
? 'Uploading...'
|
|
55
|
+
: 'Upload to Walrus'}
|
|
46
56
|
</button>
|
|
47
57
|
|
|
48
58
|
{upload.isError && <p className="error">Error: {upload.error.message}</p>}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ConnectButton } from '@mysten/dapp-kit';
|
|
2
|
+
import { useWallet } from '../../hooks/use-wallet.js';
|
|
3
|
+
|
|
4
|
+
export function WalletConnect() {
|
|
5
|
+
const { isConnected, address } = useWallet();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="wallet-connect">
|
|
9
|
+
{isConnected ? (
|
|
10
|
+
<div className="wallet-info">
|
|
11
|
+
<span>
|
|
12
|
+
Connected: {address?.slice(0, 6)}...{address?.slice(-4)}
|
|
13
|
+
</span>
|
|
14
|
+
</div>
|
|
15
|
+
) : (
|
|
16
|
+
<p>Please connect your wallet</p>
|
|
17
|
+
)}
|
|
18
|
+
<ConnectButton />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { WalletConnect } from '../features/wallet-connect.js';
|
|
3
|
+
|
|
4
|
+
interface LayoutProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function AppLayout({ children }: LayoutProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="app-layout">
|
|
11
|
+
<header className="app-header">
|
|
12
|
+
<h1><span className="text-secondary">🌊</span> <span className="text-accent">Walrus</span> App</h1>
|
|
13
|
+
<WalletConnect />
|
|
14
|
+
</header>
|
|
15
|
+
<main className="app-main">{children}</main>
|
|
16
|
+
<footer className="app-footer">
|
|
17
|
+
<p className="text-secondary">Powered by <span className="text-accent">Walrus</span> & <span style={{ color: 'var(--walrus-accent-blue)' }}>Sui</span></p>
|
|
18
|
+
</footer>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { storageAdapter } from '../lib/walrus/index.js';
|
|
3
|
+
|
|
4
|
+
export function useDownload(blobId: string | null) {
|
|
5
|
+
return useQuery({
|
|
6
|
+
queryKey: ['blob', blobId],
|
|
7
|
+
queryFn: async () => {
|
|
8
|
+
if (!blobId) throw new Error('No blob ID provided');
|
|
9
|
+
return await storageAdapter.download(blobId);
|
|
10
|
+
},
|
|
11
|
+
enabled: !!blobId,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useMetadata(blobId: string | null) {
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: ['metadata', blobId],
|
|
18
|
+
queryFn: async () => {
|
|
19
|
+
if (!blobId) throw new Error('No blob ID provided');
|
|
20
|
+
return await storageAdapter.getMetadata(blobId);
|
|
21
|
+
},
|
|
22
|
+
enabled: !!blobId,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { storageAdapter } from '../lib/walrus/index.js';
|
|
3
|
+
import type { UploadOptions, SignAndExecuteTransactionArgs } from '../lib/walrus/types.js';
|
|
4
|
+
import { useCurrentAccount, useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit';
|
|
5
|
+
|
|
6
|
+
export function useUpload() {
|
|
7
|
+
const currentAccount = useCurrentAccount();
|
|
8
|
+
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
|
|
9
|
+
const suiClient = useSuiClient();
|
|
10
|
+
|
|
11
|
+
return useMutation({
|
|
12
|
+
mutationFn: async ({
|
|
13
|
+
file,
|
|
14
|
+
options,
|
|
15
|
+
}: {
|
|
16
|
+
file: File;
|
|
17
|
+
options?: UploadOptions;
|
|
18
|
+
}) => {
|
|
19
|
+
if (!currentAccount) {
|
|
20
|
+
throw new Error('Wallet not connected. Please connect your wallet to upload files.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Wrap signAndExecute to return Promise
|
|
24
|
+
const signTransaction = (args: SignAndExecuteTransactionArgs) => {
|
|
25
|
+
return new Promise<{ digest: string }>((resolve, reject) => {
|
|
26
|
+
signAndExecute(
|
|
27
|
+
{
|
|
28
|
+
transaction: args.transaction,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
onSuccess: (result) => resolve({ digest: result.digest }),
|
|
32
|
+
onError: (error) => reject(error),
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const blobId = await storageAdapter.upload(file, {
|
|
39
|
+
...options,
|
|
40
|
+
client: suiClient,
|
|
41
|
+
signer: {
|
|
42
|
+
address: currentAccount.address,
|
|
43
|
+
signAndExecuteTransaction: signTransaction,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return { blobId, file };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|