Successfully implemented a complete seller dashboard for viewing registered IP listings and managing pending swaps. Implementation follows senior development practices with proper error handling, accessibility, and consistent design patterns.
Purpose: React hook for fetching seller listings and cross-referencing with pending swaps
Key Features:
- Fetches all listing IDs owned by seller via
ip_registry.list_by_owner() - Parallel fetches full listing details for each ID
- Fetches all swaps for seller via
atomic_swap.get_swaps_by_seller() - Enriches listings with
active_swapproperty (Pending swaps only) - 15-second polling interval with cleanup
- Proper error handling and loading states
Exports:
export function useMyListings(sellerAddress)
// Returns: { listings, loading, error, refresh }Purpose: Dashboard component showing all seller listings with pending swaps
Key Features:
- Wallet Guard: Displays connection prompt when wallet not connected
- Two sections:
- "Pending Confirmations" - Listings with active pending swaps (highest priority)
- "All Listings" - Other listings without active swaps
- ListingCard Subcomponent:
- Shows listing ID, IPFS hash (truncated), price
- Shows active swap details (buyer, amount)
- Integrates ConfirmSwapForm for swap confirmation
- "No pending swaps" message for passive listings
- Loading States:
- Skeleton screens while fetching
- Loading spinner on refresh button
- Error banner for failures
- Empty State: Message when seller has no listings
- Accessibility: Full aria-labels and semantic HTML
Purpose: Professional styling for dashboard and cards
Styling Includes:
- Header with refresh button
- Card-based layout for listings
- Pending swap badge (orange/warning color)
- Swap information highlight box
- Loading skeleton animation
- Empty state styling
- Responsive flex layout
- Hover effects
New Additions:
const IP_REGISTRY_CONTRACT_ID = import.meta.env.VITE_CONTRACT_IP_REGISTRY;// Generic simulator for any contract
async function simulateViewOnContract(contractId, functionName, args)
// Decode Listing struct from ScVal
function decodeListingScVal(scVal, listingId)export async function getListingsByOwner(ownerAddress)
// Calls: ip_registry.list_by_owner(address) -> Vec<u64>
export async function getListing(listingId)
// Reads persistent storage for Listing struct
// Returns: { id, owner, ipfs_hash, merkle_root, royalty_bps, royalty_recipient, price_usdc }
export async function getSwapsBySeller(sellerAddress)
// Calls: atomic_swap.get_swaps_by_seller(address) -> Vec<u64>Change: Added new React portal container
<div id="listings-dashboard-root"></div>Changes:
- Imported
MyListingsDashboardcomponent - Added portal rendering for listings dashboard
- Maintains shared WalletProvider context
Flow:
1. User connects wallet
↓
2. MyListingsDashboard receives address via useWallet()
↓
3. useMyListings hook executes:
a) getListingsByOwner(address) → listing IDs
b) getListing(id) for each ID in parallel → full Listing objects
c) getSwapsBySeller(address) → swap IDs
d) getSwap(id) for each swap ID in parallel → full Swap objects
e) Build activeSwapsByListing map (only Pending status)
f) Enrich listings with active_swap property
↓
4. Component renders:
- Listings with active_swap in "Pending Confirmations" section
- Other listings in "All Listings" section
- Each ListingCard shows ConfirmSwapForm for pending swaps
↓
5. Seller confirms swap:
- ConfirmSwapForm submits decryption key
- confirmSwap() executes transaction
- onSwapUpdated callback → refresh() called
- Data refetches automatically
// Get all listing IDs for owner
ip_registry.list_by_owner(owner: Address) -> Vec<u64>
// Get full listing details
ip_registry.get_listing(listing_id: u64) -> Option<Listing>
// Returns: { owner, ipfs_hash, merkle_root, royalty_bps, royalty_recipient, price_usdc }// Get all swaps where user is seller
atomic_swap.get_swaps_by_seller(seller: Address) -> Vec<u64>
// Get full swap details
atomic_swap.get_swap(swap_id: u64) -> Option<Swap>
// Returns: { listing_id, buyer, seller, status, usdc_amount, expires_at, created_at, decryption_key, ... }Add to .env file:
VITE_CONTRACT_IP_REGISTRY=<ip_registry_contract_address>
VITE_CONTRACT_ATOMIC_SWAP=<atomic_swap_contract_address>
VITE_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
VITE_STELLAR_NETWORK=testnet
✅ Contract ID validation - throws meaningful error if env variable missing ✅ Network failures - caught and displayed to user ✅ Empty contract responses - properly handled null returns ✅ Parallel fetch robustness - Promise.allSettled filters failures ✅ User feedback - error banners and loading states
✅ Semantic HTML (<section>, <h2>, <h3>)
✅ ARIA labels (aria-label, aria-busy)
✅ Role attributes (role="alert")
✅ Keyboard accessible buttons
✅ Color contrast compliant styling
✅ Hidden decorative elements (aria-hidden="true")
✅ Consistency - Matches MySwapsDashboard patterns exactly ✅ Separation of Concerns - Hook/Component/CSS properly separated ✅ Error Boundaries - User-facing error messages ✅ Loading States - Skeletal loading, spinner, empty states ✅ Performance - Parallel data fetching, 15s polling ✅ Type Safety - JSDoc comments for function signatures ✅ Memory Management - Proper cleanup (clearInterval in useEffect) ✅ Data Normalization - Consistent object structure from decoders ✅ DRY Principle - Reused ConfirmSwapForm component ✅ Portal Rendering - Proper DOM injection for React
-
Unit Tests
- Test useMyListings with mock contract calls
- Test ListingCard rendering with various states
-
Integration Tests
- Test wallet connection flow
- Test listing fetch and display
- Test pending swap highlighting
- Test confirm swap integration
-
Manual Testing
- Register test listings via ip_registry
- Create pending swaps via atomic_swap
- Connect wallet and verify listings display
- Confirm swap and verify refresh
- Pagination for sellers with many listings
- Search/filter functionality
- Sort by price, recent, etc.
- Listing deregistration UI
- Swap dispute resolution UI
- Bulk operations
- IPFS hash displayed truncated to 16 chars (show full on hover via title attribute)
- Pending swaps shown with orange badge for visibility
- Active swap section appears first to prioritize seller attention
- All swaps filtered to only show Pending status (Completed/Cancelled excluded)
- Price shows "No minimum" when price_usdc is 0
- Wallet address truncated to 10 chars in swap details for readability