Skip to content
Merged
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
138 changes: 119 additions & 19 deletions SONORA.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,24 +177,124 @@ src/layouts/
src/config/apps.ts # Add Sonora to apps list with placeholder image
```

## Technical Decisions (TO BE MADE)
- [ ] Audio format support (.mp3, .wav, .ogg)
- [ ] Streaming vs download approach
- [ ] File size limitations
- [ ] Quality options
- [ ] Offline playback support

## Implementation Notes
- Using TanStack Router for routing
- TanStack Query for data fetching
- Redux Toolkit with thunks for state management
- Tailwind CSS + Shadcn components
- Features pattern for organization
- Following existing permasearch/pinax patterns

## Discussion Notes
*This section will be updated with our decisions as we discuss each feature*
## Technical Decisions (IMPLEMENTED)
- [x] Audio format support (.mp3, .wav, .ogg, .flac, .m4a, .mpeg, .webm)
- [x] Streaming approach with HTML5 audio element
- [x] File size limitations per Pinax file type configuration
- [x] Direct playback with browser audio controls
- [x] Arweave permanent storage for audio files

## Implementation Status

### ✅ COMPLETED FEATURES

#### Core Marketplace Functionality
- **Archive Page**: User's owned audio NFTs (not listed for sale)
- **Studio Page**: User's audio NFTs listed for sale (with edit/unlist actions)
- **Market Page**: Other users' audio NFTs available for purchase
- **Working Action Buttons**: Buy, Sell, Edit Price, Unlist with dialog modals
- **ICRC-2 Integration**: Proper approval flow for secure NFT purchases

#### State Management & Data Flow
- **Redux Store**: Separate slices for archive, studio, and market
- **Pagination**: Load More functionality with page size of 8
- **Loading States**: Smart loading that preserves existing content while loading new pages
- **Error Handling**: Comprehensive error states with retry functionality

#### User Experience
- **Audio Player**: Integrated playback with visual progress indicators
- **Price Display**: Show NFT prices in ICP format
- **Owner Information**: Display NFT owner (truncated principal) on market page only
- **Filtering**: Market page excludes current user's listings automatically

#### Integration with Existing System
- **Emporium**: Marketplace transactions and listings
- **ICP Ledger**: ICRC-2 token approval and transfers
- **ICRC7**: NFT ownership verification and transfers
- **Arweave**: Audio metadata and content fetching via GraphQL

#### Browse/Discovery (Main Page)
- **Arweave Integration**: Fetches audio files from Arweave network via GraphQL
- **Minting**: Full integration with existing Permasearch minting system
- **UI**: Complete card-based interface with Load More pagination
- **Audio Playback**: Integrated audio player with progress indicators

#### Upload/Record Pages
- **Upload Flow**: Complete file selection with Pinax integration for upload and mint
- **Recording**: Full browser-based audio recording with MediaRecorder API
- **Audio Preview**: Real-time playback of recorded/uploaded content with AudioCard
- **Processing States**: Complete upload/mint workflow with error handling

### 📋 IMPLEMENTATION DETAILS

#### File Structure (COMPLETED)
```
src/features/sonora/
├── archiveSlice.ts # User's owned NFTs state
├── marketSlice.ts # Marketplace state
├── studioSlice.ts # User's listed NFTs state
├── sonoraSlice.ts # Global audio player state
├── types.ts # TypeScript interfaces
├── components/
│ ├── AudioCard.tsx # NFT display with player controls
│ ├── BuyButton.tsx # Purchase dialog with approval flow
│ ├── SellButton.tsx # List for sale dialog
│ ├── EditButton.tsx # Edit price dialog
│ ├── UnlistButton.tsx # Remove from marketplace dialog
│ ├── MintButton.tsx # Mint from Arweave (browse page)
│ └── PlayPauseButton.tsx # Audio player controls
├── hooks/
│ ├── useUserAudioNFTs.ts # Archive page data
│ ├── useStudioAudioNFTs.ts # Studio page data
│ ├── useMarketAudioNFTs.ts # Market page data
│ ├── useBuyAudio.ts # Purchase workflow with ICRC-2
│ ├── useSellAudio.ts # Listing workflow
│ ├── useUpdateAudio.ts # Price editing
│ ├── useUnlistAudio.ts # Marketplace removal
│ └── useArweaveAudios.ts # Browse page data
├── api/
│ ├── fetchUserAudioNFTs.ts # User's NFTs from multiple sources
│ ├── fetchStudioAudioNFTs.ts # User's marketplace listings
│ ├── fetchMarketAudioNFTs.ts # All marketplace listings (filtered)
│ └── fetchArweaveAudios.ts # Arweave audio discovery
└── utils/
└── audioHelpers.ts # Audio format validation & utilities
```

#### Pages Structure (COMPLETED)
```
src/pages/sonora/
├── index.tsx # Browse page (Arweave discovery)
├── ArchivePage.tsx # User's collection
├── StudioPage.tsx # User's listings
├── MarketPage.tsx # Global marketplace
├── UploadPage.tsx # File upload + mint
└── RecordPage.tsx # Audio recording + mint
```

#### Navigation & Layout (COMPLETED)
- **SonoraLayout.tsx**: Horizontal tab navigation following Exchange/Emporium pattern
- **Active Route Highlighting**: Visual indication of current page
- **Responsive Design**: Works across different screen sizes

## Technical Specifications

### Audio Formats Supported
- MP3, WAV, OGG, FLAC, M4A, MPEG, WebM
- File size limits per Pinax configuration (100MB for media)

### Blockchain Integration
- **ICP Ledger**: ICRC-2 token standard for payments
- **ICRC7**: NFT ownership and transfers
- **Emporium**: Marketplace contract for listings
- **Arweave**: Permanent storage via GraphQL queries

### State Management Architecture
- **Modular Redux Slices**: Separate state management for each page type
- **Async Thunks**: Handle API calls and error states
- **Pagination**: Append mode to preserve existing content during loading
- **Loading States**: Separate loading indicators for initial load vs. load more

---
**Last Updated:** 2025-10-26
**Status:** Detailed specification with code structure
**Last Updated:** 2025-11-11
**Status:** ✅ **FULLY COMPLETED** - Complete Sonora audio NFT marketplace with all 6 pages, working marketplace actions, ICRC-2 integration, pagination, and full blockchain integration
2 changes: 2 additions & 0 deletions src/alex_frontend/src/features/permasearch/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { ActorSubclass } from "@dfinity/agent";
import type { _SERVICE } from "../../../../../declarations/nft_manager/nft_manager.did";
import { estimateBlockHeight, fetchBlockHeightForTimestamp, getBlockHeightForTimestamp, getCurrentBlockHeight } from "./blocks";

export { getCurrentBlockHeight };

export const ARWEAVE_GRAPHQL_ENDPOINT = "https://arweave.net/graphql";

export async function filterAvailableAssets(
Expand Down
3 changes: 2 additions & 1 deletion src/alex_frontend/src/features/pinax/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const FILE_TYPES = {
"video/webm",
"audio/mpeg", // or mp3. There is also a video/mpeg which is not included
"audio/wav",
"audio/ogg"
"audio/ogg",
"audio/webm" // Support for recorded audio from MediaRecorder
],
icon: <Video className="w-7 h-7 text-muted-foreground" strokeWidth={1.5} />,
label: "Media",
Expand Down
119 changes: 119 additions & 0 deletions src/alex_frontend/src/features/pinax/hooks/useUploadAndMint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState } from 'react';
import { useAppDispatch } from '@/store/hooks/useAppDispatch';
import { useAppSelector } from '@/store/hooks/useAppSelector';
import useAlexWallet from '@/hooks/actors/useAlexWallet';
import useNftManager from '@/hooks/actors/useNftManager';
import { getFileTypeInfo } from '../constants';
import { formatFileSize } from '../utils';
import estimateCost from '../thunks/estimateCost';
import fetchWallets from '../thunks/fetchWallets';
import selectWallet from '../thunks/selectWallet';
import processPayment from '../thunks/processPayment';
import uploadFile from '../thunks/uploadFile';
import mint from '../../nft/thunks/mint';
import { reset } from '../pinaxSlice';

const validateFileType = (file: File) => {
const typeInfo = getFileTypeInfo(file.type);
if (!typeInfo) {
throw new Error(`File type ${file.type} is not supported.`);
}
};

const validateFileSize = (file: File) => {
const typeInfo = getFileTypeInfo(file.type);
if (!typeInfo) throw new Error("Invalid file type");

if (file.size > typeInfo.maxSize) {
throw new Error(
`File size ${formatFileSize(file.size)} exceeds ${formatFileSize(typeInfo.maxSize)} limit for ${typeInfo.label.toLowerCase()}.`
);
}
};

export const useUploadAndMint = () => {
const dispatch = useAppDispatch();
const { uploading, minting, estimating, progress, cost, lbryFee } = useAppSelector(
(state) => state.pinax
);
const { actor: walletActor } = useAlexWallet();
const { actor: nftActor } = useNftManager();

const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

const uploadAndMint = async (file: File) => {
setError(null);
setSuccess(null);
setLoading(true);

try {
// Step 1: Validate file type
validateFileType(file);

// Step 2: Validate file size
validateFileSize(file);

// Step 3: Estimate cost
await dispatch(estimateCost({ file })).unwrap();

// Step 4: Fetch wallets
if (!walletActor) throw new Error("No wallet connection available");
await dispatch(fetchWallets(walletActor)).unwrap();

// Step 5: Select suitable wallet
await dispatch(selectWallet()).unwrap();

// Step 6: Process payment
if (!nftActor)
throw new Error("No NFT manager connection available");
await dispatch(
processPayment({ fileSizeBytes: file.size, actor: nftActor })
).unwrap();

// Step 7: Upload file
const transaction = await dispatch(
uploadFile({ file, actor: walletActor })
).unwrap();

// Step 8: Mint NFT
await dispatch(mint({ transaction, actor: nftActor })).unwrap();

setSuccess(
`File uploaded successfully! Transaction ID: ${transaction}`
);

return transaction;
} catch (err: any) {
setError(err.message || err || "An unknown error occurred");
throw err;
} finally {
setLoading(false);
}
};

const resetUpload = () => {
setError(null);
setSuccess(null);
setLoading(false);
dispatch(reset());
};

const isProcessing = uploading || minting || estimating || loading;

return {
uploadAndMint,
resetUpload,
isProcessing,
uploading,
minting,
estimating,
progress,
cost,
lbryFee,
error,
success,
loading
};
};
Loading