@blu1606/create-walrus-app 0.1.4 → 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.
Files changed (129) hide show
  1. package/dist/generator/file-ops.d.ts +8 -0
  2. package/dist/generator/file-ops.js +20 -0
  3. package/dist/generator/index.js +37 -22
  4. package/dist/generator/layers.d.ts +15 -2
  5. package/dist/generator/layers.js +38 -47
  6. package/dist/generator/types.d.ts +9 -1
  7. package/dist/index.js +1 -2
  8. package/dist/post-install/git.d.ts +8 -0
  9. package/dist/post-install/git.js +2 -0
  10. package/dist/post-install/index.d.ts +0 -1
  11. package/dist/post-install/index.js +2 -15
  12. package/dist/post-install/messages.js +1 -1
  13. package/package.json +68 -68
  14. package/{templates/base → presets/react-mysten-gallery}/.env.example +31 -31
  15. package/presets/react-mysten-gallery/.gitkeep +4 -0
  16. package/{templates/gallery → presets/react-mysten-gallery}/README.md +25 -22
  17. package/presets/react-mysten-gallery/package.json +34 -0
  18. package/presets/react-mysten-gallery/src/App.tsx +23 -0
  19. package/presets/react-mysten-gallery/src/components/features/file-card.tsx +89 -0
  20. package/{templates/gallery/src/components/GalleryGrid.tsx → presets/react-mysten-gallery/src/components/features/gallery-grid.tsx} +5 -5
  21. package/presets/react-mysten-gallery/src/components/features/upload-modal.tsx +69 -0
  22. package/{templates/react/src/components/WalletConnect.tsx → presets/react-mysten-gallery/src/components/features/wallet-connect.tsx} +1 -1
  23. package/presets/react-mysten-gallery/src/components/layout/app-layout.tsx +21 -0
  24. package/{templates/react/src/hooks/useStorage.ts → presets/react-mysten-gallery/src/hooks/use-download.ts} +2 -18
  25. package/presets/react-mysten-gallery/src/hooks/use-upload.ts +49 -0
  26. package/{templates/react/src/hooks/useWallet.ts → presets/react-mysten-gallery/src/hooks/use-wallet.ts} +2 -7
  27. package/presets/react-mysten-gallery/src/index.css +384 -0
  28. package/presets/react-mysten-gallery/src/index.ts +17 -0
  29. package/presets/react-mysten-gallery/src/lib/walrus/adapter.ts +197 -0
  30. package/presets/react-mysten-gallery/src/lib/walrus/client.ts +87 -0
  31. package/presets/react-mysten-gallery/src/lib/walrus/index.ts +4 -0
  32. package/presets/react-mysten-gallery/src/lib/walrus/types.ts +101 -0
  33. package/{templates/react → presets/react-mysten-gallery}/src/main.tsx +0 -1
  34. package/{templates/react → presets/react-mysten-gallery}/src/providers/WalletProvider.tsx +16 -1
  35. package/{templates/base → presets/react-mysten-gallery}/src/utils/env.ts +41 -41
  36. package/{templates/gallery → presets/react-mysten-gallery}/src/utils/index-manager.ts +2 -2
  37. package/presets/react-mysten-gallery/src/utils/mime-type.ts +97 -0
  38. package/presets/react-mysten-gallery/src/utils/preview-generator.ts +134 -0
  39. package/{templates/react → presets/react-mysten-gallery}/tsconfig.json +20 -8
  40. package/presets/react-mysten-simple-upload/.env.example +31 -0
  41. package/presets/react-mysten-simple-upload/.gitkeep +4 -0
  42. package/presets/react-mysten-simple-upload/index.html +13 -0
  43. package/{templates/react → presets/react-mysten-simple-upload}/package.json +13 -11
  44. package/presets/react-mysten-simple-upload/src/App.tsx +27 -0
  45. package/presets/react-mysten-simple-upload/src/components/features/file-preview.tsx +73 -0
  46. package/{templates/simple-upload/src/components/UploadForm.tsx → presets/react-mysten-simple-upload/src/components/features/upload-form.tsx} +15 -5
  47. package/presets/react-mysten-simple-upload/src/components/features/wallet-connect.tsx +21 -0
  48. package/presets/react-mysten-simple-upload/src/components/layout/app-layout.tsx +21 -0
  49. package/presets/react-mysten-simple-upload/src/hooks/use-download.ts +24 -0
  50. package/presets/react-mysten-simple-upload/src/hooks/use-upload.ts +49 -0
  51. package/presets/react-mysten-simple-upload/src/hooks/use-wallet.ts +11 -0
  52. package/presets/react-mysten-simple-upload/src/index.css +252 -0
  53. package/presets/react-mysten-simple-upload/src/index.ts +16 -0
  54. package/presets/react-mysten-simple-upload/src/lib/walrus/adapter.ts +197 -0
  55. package/presets/react-mysten-simple-upload/src/lib/walrus/client.ts +87 -0
  56. package/presets/react-mysten-simple-upload/src/lib/walrus/index.ts +4 -0
  57. package/{templates/base/src/adapters/storage.ts → presets/react-mysten-simple-upload/src/lib/walrus/types.ts} +83 -58
  58. package/presets/react-mysten-simple-upload/src/main.tsx +16 -0
  59. package/presets/react-mysten-simple-upload/src/providers/QueryProvider.tsx +18 -0
  60. package/presets/react-mysten-simple-upload/src/providers/WalletProvider.tsx +52 -0
  61. package/presets/react-mysten-simple-upload/src/utils/env.ts +41 -0
  62. package/presets/react-mysten-simple-upload/src/utils/mime-type.ts +97 -0
  63. package/presets/react-mysten-simple-upload/tsconfig.json +39 -0
  64. package/presets/react-mysten-simple-upload/tsconfig.node.json +10 -0
  65. package/presets/react-mysten-simple-upload/vite.config.ts +19 -0
  66. package/dist/__tests__/helpers/adapter-compliance.d.ts +0 -2
  67. package/dist/__tests__/helpers/adapter-compliance.js +0 -47
  68. package/dist/__tests__/helpers/fixtures.d.ts +0 -21
  69. package/dist/__tests__/helpers/fixtures.js +0 -30
  70. package/dist/__tests__/helpers/fs-helpers.d.ts +0 -12
  71. package/dist/__tests__/helpers/fs-helpers.js +0 -35
  72. package/dist/__tests__/helpers/index.d.ts +0 -4
  73. package/dist/__tests__/helpers/index.js +0 -4
  74. package/dist/__tests__/helpers/test-hooks.d.ts +0 -3
  75. package/dist/__tests__/helpers/test-hooks.js +0 -18
  76. package/dist/context.test.d.ts +0 -1
  77. package/dist/context.test.js +0 -98
  78. package/dist/generator/index.test.d.ts +0 -1
  79. package/dist/generator/index.test.js +0 -143
  80. package/dist/generator/layers.test.d.ts +0 -1
  81. package/dist/generator/layers.test.js +0 -92
  82. package/dist/generator/merge.test.d.ts +0 -1
  83. package/dist/generator/merge.test.js +0 -79
  84. package/dist/generator/transform.test.d.ts +0 -1
  85. package/dist/generator/transform.test.js +0 -51
  86. package/dist/matrix.test.d.ts +0 -1
  87. package/dist/matrix.test.js +0 -70
  88. package/dist/types.test.d.ts +0 -1
  89. package/dist/types.test.js +0 -65
  90. package/dist/utils/detect-pm.test.d.ts +0 -1
  91. package/dist/utils/detect-pm.test.js +0 -52
  92. package/dist/validator.test.d.ts +0 -1
  93. package/dist/validator.test.js +0 -96
  94. package/templates/base/README.md +0 -54
  95. package/templates/base/package.json +0 -19
  96. package/templates/base/src/types/index.ts +0 -9
  97. package/templates/base/src/types/walrus.ts +0 -22
  98. package/templates/base/src/utils/format.ts +0 -29
  99. package/templates/base/tsconfig.json +0 -19
  100. package/templates/gallery/package.json +0 -6
  101. package/templates/gallery/src/App.tsx +0 -21
  102. package/templates/gallery/src/components/FileCard.tsx +0 -27
  103. package/templates/gallery/src/components/UploadModal.tsx +0 -45
  104. package/templates/gallery/src/styles.css +0 -58
  105. package/templates/gallery/src/types/gallery.ts +0 -13
  106. package/templates/react/.eslintrc.json +0 -26
  107. package/templates/react/README.md +0 -80
  108. package/templates/react/src/App.tsx +0 -14
  109. package/templates/react/src/components/Layout.tsx +0 -21
  110. package/templates/react/src/dapp-kit.css +0 -1
  111. package/templates/react/src/index.css +0 -50
  112. package/templates/react/src/index.ts +0 -10
  113. package/templates/sdk-mysten/README.md +0 -65
  114. package/templates/sdk-mysten/package.json +0 -14
  115. package/templates/sdk-mysten/src/adapter.ts +0 -80
  116. package/templates/sdk-mysten/src/client.ts +0 -45
  117. package/templates/sdk-mysten/src/config.ts +0 -33
  118. package/templates/sdk-mysten/src/index.ts +0 -11
  119. package/templates/sdk-mysten/src/types.ts +0 -19
  120. package/templates/sdk-mysten/test/adapter.test.ts +0 -20
  121. package/templates/simple-upload/package.json +0 -6
  122. package/templates/simple-upload/src/App.tsx +0 -27
  123. package/templates/simple-upload/src/components/FilePreview.tsx +0 -40
  124. package/templates/simple-upload/src/styles.css +0 -33
  125. /package/{templates/react → presets/react-mysten-gallery}/index.html +0 -0
  126. /package/{templates/react → presets/react-mysten-gallery}/src/providers/QueryProvider.tsx +0 -0
  127. /package/{templates/react → presets/react-mysten-gallery}/tsconfig.node.json +0 -0
  128. /package/{templates/react → presets/react-mysten-gallery}/vite.config.ts +0 -0
  129. /package/{templates/simple-upload → presets/react-mysten-simple-upload}/README.md +0 -0
@@ -0,0 +1,197 @@
1
+ import type {
2
+ StorageAdapter,
3
+ BlobMetadata,
4
+ UploadOptions,
5
+ DownloadOptions,
6
+ } from './types.js';
7
+ import { getWalrusClient, resetWalrusClient } from './client.js';
8
+ import { WalrusFile, RetryableWalrusClientError } from '@mysten/walrus';
9
+ import { getContentType } from '../../utils/mime-type.js';
10
+
11
+ export class MystenStorageAdapter implements StorageAdapter {
12
+ async upload(
13
+ data: File | Uint8Array,
14
+ options?: UploadOptions
15
+ ): Promise<string> {
16
+ const bytes =
17
+ data instanceof File ? new Uint8Array(await data.arrayBuffer()) : data;
18
+ const fileName = data instanceof File ? data.name : 'upload.bin';
19
+
20
+ try {
21
+ // Check if signer and client are provided
22
+ if (!options?.signer) {
23
+ throw new Error('Wallet signer is required for upload. Please connect your wallet.');
24
+ }
25
+ if (!options?.client) {
26
+ throw new Error('Sui client is required for upload.');
27
+ }
28
+
29
+ const client = options.client;
30
+
31
+ console.log('[Upload] Starting upload flow...');
32
+
33
+ // Detect content type with fallback logic
34
+ const contentType = getContentType(
35
+ data,
36
+ options?.contentType
37
+ );
38
+
39
+ console.log(`[Upload] Detected content-type: ${contentType} for file: ${fileName}`);
40
+
41
+ // Create upload flow with full metadata (identifier + tags)
42
+ const flow = client.walrus.writeFilesFlow({
43
+ files: [
44
+ WalrusFile.from({
45
+ contents: bytes,
46
+ identifier: fileName, // Original filename with extension
47
+ tags: {
48
+ 'content-type': contentType,
49
+ 'original-name': fileName,
50
+ },
51
+ }),
52
+ ],
53
+ });
54
+
55
+ // Step 1: Encode the file
56
+ console.log('[Upload] Step 1: Encoding file...');
57
+ await flow.encode();
58
+ console.log('[Upload] Step 1: Encode complete');
59
+
60
+ // Step 2: Register the blob on-chain (requires wallet signature)
61
+ console.log('[Upload] Step 2: Creating register transaction...');
62
+ const registerTx = flow.register({
63
+ epochs: options?.epochs || 1,
64
+ owner: options.signer.address,
65
+ deletable: true,
66
+ });
67
+
68
+ // Sign and execute the register transaction (dapp-kit will handle wrapping)
69
+ console.log('[Upload] Step 2: Signing register transaction...');
70
+ const registerResult = await options.signer.signAndExecuteTransaction({
71
+ transaction: registerTx,
72
+ });
73
+ console.log('[Upload] Step 2: Register complete, digest:', registerResult.digest);
74
+
75
+ // Step 3: Upload the data to storage nodes
76
+ console.log('[Upload] Step 3: Uploading to storage nodes...');
77
+ await flow.upload({ digest: registerResult.digest });
78
+ console.log('[Upload] Step 3: Upload complete');
79
+
80
+ // Step 4: Certify the blob (requires wallet signature)
81
+ console.log('[Upload] Step 4: Creating certify transaction...');
82
+ const certifyTx = flow.certify();
83
+
84
+ // Sign and execute the certify transaction (dapp-kit will handle wrapping)
85
+ console.log('[Upload] Step 4: Signing certify transaction...');
86
+ await options.signer.signAndExecuteTransaction({
87
+ transaction: certifyTx,
88
+ });
89
+ console.log('[Upload] Step 4: Certify complete');
90
+
91
+ // Step 5: Get the uploaded files
92
+ console.log('[Upload] Step 5: Getting file list...');
93
+ const files = await flow.listFiles();
94
+
95
+ if (!files || files.length === 0) {
96
+ throw new Error('Upload completed but no files were returned');
97
+ }
98
+
99
+ const blobId = files[0].blobId;
100
+ console.log('[Upload] Success! Blob ID:', blobId);
101
+ return blobId;
102
+ } catch (error) {
103
+ console.error('[Upload] Error details:', error);
104
+ throw new Error(
105
+ `Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`
106
+ );
107
+ }
108
+ }
109
+
110
+ async download(
111
+ blobId: string,
112
+ options?: DownloadOptions
113
+ ): Promise<Uint8Array | string | unknown> {
114
+ const client = getWalrusClient();
115
+
116
+ try {
117
+ // Use getFiles for better API - supports future batch downloads
118
+ const [file] = await client.walrus.getFiles({ ids: [blobId] });
119
+
120
+ // Handle different output formats
121
+ switch (options?.format) {
122
+ case 'text':
123
+ return await file.text();
124
+ case 'json':
125
+ return await file.json();
126
+ default:
127
+ return await file.bytes();
128
+ }
129
+ } catch (error) {
130
+ // Handle retryable errors by resetting client and retrying once
131
+ if (error instanceof RetryableWalrusClientError) {
132
+ console.warn(`[Download] Retryable error detected, resetting client and retrying...`);
133
+ resetWalrusClient();
134
+
135
+ try {
136
+ const retryClient = getWalrusClient();
137
+ const [file] = await retryClient.walrus.getFiles({ ids: [blobId] });
138
+
139
+ switch (options?.format) {
140
+ case 'text':
141
+ return await file.text();
142
+ case 'json':
143
+ return await file.json();
144
+ default:
145
+ return await file.bytes();
146
+ }
147
+ } catch (retryError) {
148
+ throw new Error(
149
+ `Download retry failed for blob ${blobId}: ${retryError instanceof Error ? retryError.message : 'Unknown error'}`
150
+ );
151
+ }
152
+ }
153
+
154
+ throw new Error(
155
+ `Download failed for blob ${blobId}: ${error instanceof Error ? error.message : 'Unknown error'}`
156
+ );
157
+ }
158
+ }
159
+
160
+ async getMetadata(blobId: string): Promise<BlobMetadata> {
161
+ const client = getWalrusClient();
162
+
163
+ try {
164
+ // Get metadata from Walrus
165
+ const metadata = await client.walrus.getBlobMetadata({ blobId });
166
+
167
+ // Get WalrusFile to extract identifier and tags
168
+ const [file] = await client.walrus.getFiles({ ids: [blobId] });
169
+ const identifier = await file.getIdentifier();
170
+ const tags = await file.getTags();
171
+
172
+ return {
173
+ blobId,
174
+ size: Number(metadata.metadata.V1.unencoded_length),
175
+ contentType: tags['content-type'] || 'application/octet-stream',
176
+ fileName: tags['original-name'] || identifier || `blob-${blobId.slice(0, 8)}`,
177
+ tags,
178
+ createdAt: (metadata as unknown as { createdAt?: number }).createdAt || Date.now(),
179
+ };
180
+ } catch (error) {
181
+ throw new Error(
182
+ `Failed to get metadata for blob ${blobId}: ${error instanceof Error ? error.message : 'Unknown error'}`
183
+ );
184
+ }
185
+ }
186
+
187
+ async exists(blobId: string): Promise<boolean> {
188
+ try {
189
+ await this.getMetadata(blobId);
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ }
196
+
197
+ export const storageAdapter = new MystenStorageAdapter();
@@ -0,0 +1,87 @@
1
+ import { walrus, WalrusClient } from '@mysten/walrus';
2
+ import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
3
+ import { loadEnv } from '../../utils/env.js';
4
+
5
+ // Types
6
+ export type WalrusNetwork = 'testnet' | 'mainnet';
7
+
8
+ export interface MystenWalrusConfig {
9
+ network: WalrusNetwork;
10
+ publisherUrl?: string;
11
+ aggregatorUrl?: string;
12
+ suiRpcUrl?: string;
13
+ }
14
+
15
+ type WalrusExtendedClient = SuiClient & { walrus: WalrusClient };
16
+
17
+ // Network Configurations
18
+ const NETWORK_CONFIGS: Record<WalrusNetwork, MystenWalrusConfig> = {
19
+ testnet: {
20
+ network: 'testnet',
21
+ publisherUrl: 'https://publisher.walrus-testnet.walrus.space',
22
+ aggregatorUrl: 'https://aggregator.walrus-testnet.walrus.space',
23
+ suiRpcUrl: 'https://fullnode.testnet.sui.io:443',
24
+ },
25
+ mainnet: {
26
+ network: 'mainnet',
27
+ publisherUrl: 'https://publisher.walrus.space',
28
+ aggregatorUrl: 'https://aggregator.walrus.space',
29
+ suiRpcUrl: 'https://fullnode.mainnet.sui.io:443',
30
+ },
31
+ };
32
+
33
+ function getNetworkConfig(network: WalrusNetwork): MystenWalrusConfig {
34
+ return NETWORK_CONFIGS[network];
35
+ }
36
+
37
+ // Client Management
38
+ let walrusClient: WalrusExtendedClient | null = null;
39
+
40
+ export function getWalrusClient(): WalrusExtendedClient {
41
+ if (walrusClient) {
42
+ return walrusClient;
43
+ }
44
+
45
+ const env = loadEnv();
46
+
47
+ // Validate network value before casting
48
+ const allowedNetworks: WalrusNetwork[] = ['testnet', 'mainnet'];
49
+ if (!allowedNetworks.includes(env.walrusNetwork as WalrusNetwork)) {
50
+ throw new Error(
51
+ `Invalid WALRUS_NETWORK: ${env.walrusNetwork}. Must be one of: ${allowedNetworks.join(', ')}`
52
+ );
53
+ }
54
+ const network = env.walrusNetwork as WalrusNetwork;
55
+ const config = getNetworkConfig(network);
56
+
57
+ // Use SuiClient (required for Walrus SDK and CoinWithBalance intent)
58
+ const suiClient = new SuiClient({
59
+ url:
60
+ env.suiRpc ||
61
+ config.suiRpcUrl ||
62
+ getFullnodeUrl(network === 'testnet' ? 'testnet' : 'mainnet'),
63
+ });
64
+
65
+ // Extend the client with Walrus capabilities and upload relay
66
+ walrusClient = suiClient.$extend(
67
+ walrus({
68
+ network: network, // REQUIRED: Walrus SDK needs to know which network
69
+ // Use CDN for WASM - more reliable than bundler resolution
70
+ wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm',
71
+ uploadRelay: {
72
+ host:
73
+ env.walrusPublisher ||
74
+ config.publisherUrl ||
75
+ `https://upload-relay.${network}.walrus.space`,
76
+ sendTip: null, // Skip tip config fetch to avoid CORS issues in development
77
+ },
78
+ ...(env.walrusAggregator && { aggregatorUrl: env.walrusAggregator }),
79
+ })
80
+ );
81
+
82
+ return walrusClient;
83
+ }
84
+
85
+ export function resetWalrusClient(): void {
86
+ walrusClient = null;
87
+ }
@@ -0,0 +1,4 @@
1
+ // Walrus library exports
2
+ export * from './types.js';
3
+ export * from './adapter.js';
4
+ export * from './client.js';
@@ -1,58 +1,83 @@
1
- /**
2
- * Universal storage adapter interface for Walrus
3
- *
4
- * This interface abstracts SDK-specific implementations,
5
- * allowing use case layers to work with any Walrus SDK.
6
- */
7
-
8
- export interface BlobMetadata {
9
- blobId: string;
10
- size: number;
11
- contentType?: string;
12
- createdAt: number;
13
- expiresAt?: number;
14
- }
15
-
16
- export interface UploadOptions {
17
- /** Number of epochs to store (Walrus-specific) */
18
- epochs?: number;
19
- /** MIME type of the content */
20
- contentType?: string;
21
- }
22
-
23
- export interface DownloadOptions {
24
- /** Byte range (for large files) */
25
- range?: { start: number; end: number };
26
- }
27
-
28
- export interface StorageAdapter {
29
- /**
30
- * Upload data to Walrus storage
31
- * @param data - File or raw bytes to upload
32
- * @param options - Upload configuration
33
- * @returns Blob ID (permanent reference)
34
- */
35
- upload(data: File | Uint8Array, options?: UploadOptions): Promise<string>;
36
-
37
- /**
38
- * Download blob data by ID
39
- * @param blobId - Unique blob identifier
40
- * @param options - Download configuration
41
- * @returns Raw blob data
42
- */
43
- download(blobId: string, options?: DownloadOptions): Promise<Uint8Array>;
44
-
45
- /**
46
- * Get blob metadata without downloading content
47
- * @param blobId - Unique blob identifier
48
- * @returns Metadata object
49
- */
50
- getMetadata(blobId: string): Promise<BlobMetadata>;
51
-
52
- /**
53
- * Check if blob exists
54
- * @param blobId - Unique blob identifier
55
- * @returns True if blob is accessible
56
- */
57
- exists(blobId: string): Promise<boolean>;
58
- }
1
+ /**
2
+ * Universal storage adapter interface for Walrus
3
+ *
4
+ * This interface abstracts SDK-specific implementations,
5
+ * allowing use case layers to work with any Walrus SDK.
6
+ */
7
+
8
+ import type { SuiClient } from '@mysten/sui/client';
9
+ import type { Transaction } from '@mysten/sui/transactions';
10
+
11
+ export interface BlobMetadata {
12
+ blobId: string;
13
+ size: number;
14
+ contentType?: string;
15
+ createdAt: number;
16
+ expiresAt?: number;
17
+ /** Original filename with extension */
18
+ fileName?: string;
19
+ /** All tags from WalrusFile */
20
+ tags?: Record<string, string>;
21
+ }
22
+
23
+ export interface SignAndExecuteTransactionArgs {
24
+ transaction: Transaction;
25
+ }
26
+
27
+ export interface SignAndExecuteTransactionResult {
28
+ digest: string;
29
+ effects?: unknown;
30
+ }
31
+
32
+ export interface UploadOptions {
33
+ /** Number of epochs to store (Walrus-specific) */
34
+ epochs?: number;
35
+ /** MIME type of the content */
36
+ contentType?: string;
37
+ /** Sui client with Walrus extension (from useSuiClient hook) */
38
+ client?: SuiClient;
39
+ /** Wallet signer for authenticated uploads with address and transaction signing */
40
+ signer?: {
41
+ address: string;
42
+ signAndExecuteTransaction: (args: SignAndExecuteTransactionArgs) => Promise<SignAndExecuteTransactionResult>;
43
+ };
44
+ }
45
+
46
+ export interface DownloadOptions {
47
+ /** Byte range (for large files) */
48
+ range?: { start: number; end: number };
49
+ /** Output format - auto-detects if not specified */
50
+ format?: 'bytes' | 'text' | 'json';
51
+ }
52
+
53
+ export interface StorageAdapter {
54
+ /**
55
+ * Upload data to Walrus storage
56
+ * @param data - File or raw bytes to upload
57
+ * @param options - Upload configuration
58
+ * @returns Blob ID (permanent reference)
59
+ */
60
+ upload(data: File | Uint8Array, options?: UploadOptions): Promise<string>;
61
+
62
+ /**
63
+ * Download blob data by ID
64
+ * @param blobId - Unique blob identifier
65
+ * @param options - Download configuration
66
+ * @returns Blob data in requested format (Uint8Array, string, or JSON)
67
+ */
68
+ download(blobId: string, options?: DownloadOptions): Promise<Uint8Array | string | unknown>;
69
+
70
+ /**
71
+ * Get blob metadata without downloading content
72
+ * @param blobId - Unique blob identifier
73
+ * @returns Metadata object
74
+ */
75
+ getMetadata(blobId: string): Promise<BlobMetadata>;
76
+
77
+ /**
78
+ * Check if blob exists
79
+ * @param blobId - Unique blob identifier
80
+ * @returns True if blob is accessible
81
+ */
82
+ exists(blobId: string): Promise<boolean>;
83
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { QueryProvider } from './providers/QueryProvider.js';
4
+ import { WalletProvider } from './providers/WalletProvider.js';
5
+ import App from './App.js';
6
+ import './index.css';
7
+
8
+ ReactDOM.createRoot(document.getElementById('root')!).render(
9
+ <React.StrictMode>
10
+ <QueryProvider>
11
+ <WalletProvider>
12
+ <App />
13
+ </WalletProvider>
14
+ </QueryProvider>
15
+ </React.StrictMode>
16
+ );
@@ -0,0 +1,18 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { ReactNode } from 'react';
3
+
4
+ const queryClient = new QueryClient({
5
+ defaultOptions: {
6
+ queries: {
7
+ refetchOnWindowFocus: false,
8
+ retry: 1,
9
+ staleTime: 5 * 60 * 1000,
10
+ },
11
+ },
12
+ });
13
+
14
+ export function QueryProvider({ children }: { children: ReactNode }) {
15
+ return (
16
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
17
+ );
18
+ }
@@ -0,0 +1,52 @@
1
+ import {
2
+ createNetworkConfig,
3
+ SuiClientProvider,
4
+ WalletProvider as SuiWalletProvider,
5
+ } from '@mysten/dapp-kit';
6
+ import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
7
+ import { walrus } from '@mysten/walrus';
8
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
9
+ import { ReactNode } from 'react';
10
+ import { loadEnv } from '../utils/env.js';
11
+
12
+ const env = loadEnv();
13
+
14
+ const validatedNetwork =
15
+ env.suiNetwork === 'mainnet' || env.suiNetwork === 'testnet'
16
+ ? env.suiNetwork
17
+ : 'testnet';
18
+
19
+ const { networkConfig } = createNetworkConfig({
20
+ [validatedNetwork]: {
21
+ url: env.suiRpc || getFullnodeUrl(validatedNetwork),
22
+ },
23
+ });
24
+
25
+ const walletQueryClient = new QueryClient();
26
+
27
+ export function WalletProvider({ children }: { children: ReactNode }) {
28
+ return (
29
+ <QueryClientProvider client={walletQueryClient}>
30
+ <SuiClientProvider
31
+ networks={networkConfig}
32
+ defaultNetwork={validatedNetwork}
33
+ createClient={(network: string | number, config: { readonly url: string }) => {
34
+ // Create SuiClient with Walrus extension
35
+ const client = new SuiClient({
36
+ url: config.url,
37
+ }).$extend(
38
+ walrus({
39
+ network: network as 'testnet' | 'mainnet',
40
+ wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm',
41
+ // No uploadRelay - upload directly to storage nodes
42
+ // This avoids tip payment requirements but uses ~2200 requests
43
+ })
44
+ );
45
+ return client;
46
+ }}
47
+ >
48
+ <SuiWalletProvider>{children}</SuiWalletProvider>
49
+ </SuiClientProvider>
50
+ </QueryClientProvider>
51
+ );
52
+ }
@@ -0,0 +1,41 @@
1
+ export interface EnvConfig {
2
+ walrusNetwork: string;
3
+ walrusAggregator: string;
4
+ walrusPublisher: string;
5
+ suiNetwork: string;
6
+ suiRpc: string;
7
+ blockberryKey?: string;
8
+ }
9
+
10
+ export function loadEnv(): EnvConfig {
11
+ const getEnv = (key: string, required = true): string => {
12
+ const value = import.meta.env[key];
13
+ if (required && !value) {
14
+ throw new Error(`Missing required environment variable: ${key}`);
15
+ }
16
+ return value || '';
17
+ };
18
+
19
+ return {
20
+ walrusNetwork: getEnv('VITE_WALRUS_NETWORK'),
21
+ walrusAggregator: getEnv('VITE_WALRUS_AGGREGATOR'),
22
+ walrusPublisher: getEnv('VITE_WALRUS_PUBLISHER'),
23
+ suiNetwork: getEnv('VITE_SUI_NETWORK'),
24
+ suiRpc: getEnv('VITE_SUI_RPC'),
25
+ blockberryKey: getEnv('VITE_BLOCKBERRY_KEY', false),
26
+ };
27
+ }
28
+
29
+ export function validateEnv(config: EnvConfig): void {
30
+ if (!['testnet', 'mainnet', 'devnet'].includes(config.walrusNetwork)) {
31
+ throw new Error(`Invalid WALRUS_NETWORK: ${config.walrusNetwork}`);
32
+ }
33
+
34
+ if (!config.walrusAggregator.startsWith('http')) {
35
+ throw new Error('WALRUS_AGGREGATOR must be a valid HTTP URL');
36
+ }
37
+
38
+ if (!config.walrusPublisher.startsWith('http')) {
39
+ throw new Error('WALRUS_PUBLISHER must be a valid HTTP URL');
40
+ }
41
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * MIME type detection from file extensions
3
+ * Handles common file types that browsers may not auto-detect
4
+ */
5
+
6
+ const MIME_TYPES: Record<string, string> = {
7
+ // Text formats
8
+ '.txt': 'text/plain',
9
+ '.md': 'text/markdown',
10
+ '.markdown': 'text/markdown',
11
+ '.csv': 'text/csv',
12
+ '.html': 'text/html',
13
+ '.css': 'text/css',
14
+ '.js': 'text/javascript',
15
+ '.ts': 'text/typescript',
16
+ '.json': 'application/json',
17
+ '.xml': 'application/xml',
18
+
19
+ // Images
20
+ '.jpg': 'image/jpeg',
21
+ '.jpeg': 'image/jpeg',
22
+ '.png': 'image/png',
23
+ '.gif': 'image/gif',
24
+ '.svg': 'image/svg+xml',
25
+ '.webp': 'image/webp',
26
+ '.ico': 'image/x-icon',
27
+
28
+ // Documents
29
+ '.pdf': 'application/pdf',
30
+ '.doc': 'application/msword',
31
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
32
+ '.xls': 'application/vnd.ms-excel',
33
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
34
+ '.ppt': 'application/vnd.ms-powerpoint',
35
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
36
+
37
+ // Archives
38
+ '.zip': 'application/zip',
39
+ '.rar': 'application/x-rar-compressed',
40
+ '.tar': 'application/x-tar',
41
+ '.gz': 'application/gzip',
42
+ '.7z': 'application/x-7z-compressed',
43
+
44
+ // Audio
45
+ '.mp3': 'audio/mpeg',
46
+ '.wav': 'audio/wav',
47
+ '.ogg': 'audio/ogg',
48
+ '.m4a': 'audio/mp4',
49
+
50
+ // Video
51
+ '.mp4': 'video/mp4',
52
+ '.webm': 'video/webm',
53
+ '.avi': 'video/x-msvideo',
54
+ '.mov': 'video/quicktime',
55
+ };
56
+
57
+ /**
58
+ * Detect MIME type from filename
59
+ * @param fileName - File name with extension
60
+ * @returns MIME type or null if not found
61
+ */
62
+ export function getMimeTypeFromFileName(fileName: string): string | null {
63
+ const extension = fileName.toLowerCase().match(/\.[^.]+$/)?.[0];
64
+ if (!extension) return null;
65
+
66
+ return MIME_TYPES[extension] || null;
67
+ }
68
+
69
+ /**
70
+ * Get content type with fallback logic
71
+ * Priority: explicit contentType > File.type > extension detection > fallback
72
+ * @param file - File object, Uint8Array, or filename
73
+ * @param explicitType - Explicitly provided content type
74
+ * @returns Best-match content type
75
+ */
76
+ export function getContentType(
77
+ file: File | Uint8Array | string,
78
+ explicitType?: string
79
+ ): string {
80
+ // Priority 1: Explicit content type
81
+ if (explicitType) return explicitType;
82
+
83
+ // Priority 2: File.type (if not empty and not generic)
84
+ if (file instanceof File && file.type && file.type !== 'application/octet-stream') {
85
+ return file.type;
86
+ }
87
+
88
+ // Priority 3: Detect from filename extension
89
+ const fileName = typeof file === 'string' ? file : file instanceof File ? file.name : '';
90
+ if (fileName) {
91
+ const detectedType = getMimeTypeFromFileName(fileName);
92
+ if (detectedType) return detectedType;
93
+ }
94
+
95
+ // Fallback: Generic binary
96
+ return 'application/octet-stream';
97
+ }