diff --git a/package.json b/package.json index 89778f392..81ec83be6 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,13 @@ "@dfinity/auth-client": "^3.2.7", "@dfinity/candid": "^3.2.7", "@dfinity/identity": "^3.2.7", + "@dfinity/identity-secp256k1": "^3.4.3", "@dfinity/ledger-icp": "^6.0.1", "@dfinity/principal": "^3.2.7", "@dfinity/utils": "^3.1.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", @@ -72,7 +76,6 @@ "@tanstack/react-virtual": "^3.12.0", "@tensorflow/tfjs": "^4.21.0", "@types/dompurify": "^3.0.5", - "@types/react-beautiful-dnd": "^13.1.8", "@xstate/store": "^3.9.2", "antd": "^5.20.5", "arweave": "^1.15.5", @@ -101,7 +104,7 @@ "jwk-to-pem": "^2.0.7", "lodash": "^4.17.21", "lru-cache": "^11.0.2", - "lucide-react": "^0.539.0", + "lucide-react": "^0.577.0", "meilisearch": "^0.42.0", "nanoid": "^5.0.7", "next-themes": "^0.3.0", @@ -113,7 +116,6 @@ "path-browserify": "^1.0.1", "postcss": "^8.4.45", "react": "^18.3.1", - "react-beautiful-dnd": "^13.1.1", "react-circle-flags": "^0.0.20", "react-csv": "^2.2.2", "react-day-picker": "^8.10.1", diff --git a/src/alex_frontend/core/apps/Modules/shared/components/AssetManager.tsx b/src/alex_frontend/core/apps/Modules/shared/components/AssetManager.tsx deleted file mode 100644 index 499a835ce..000000000 --- a/src/alex_frontend/core/apps/Modules/shared/components/AssetManager.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import React, { useEffect, useState } from "react"; -import { - createAssetCanister, - getCallerAssetCanister, - getAssetList, - syncNfts, - syncProgressInterface, - getCanisterCycles, -} from "../state/assetManager/assetManagerThunks"; -import { LoaderPinwheel } from "lucide-react"; -import { Button } from "@/lib/components/button"; -import { Description, FiltersButton } from "@/apps/Modules/shared/styles"; - -const AssetManager = () => { - const dispatch = useAppDispatch(); - const user = useAppSelector((state) => state.auth); - const nftData = useAppSelector((state) => state.nftData); - const assetManager = useAppSelector((state) => state.assetManager); - const { selectedPrincipals } = useAppSelector((state) => state.library); - const [userAssetCanister, setUserAssetCanister] = useState( - null - ); - const [syncProgress, setSyncProgress] = useState({ - currentItem: "", - progress: 0, // Default progress - totalSynced: 0, - currentProgress: 0, - }); - const createUserAssetCanister = () => { - if (!user.user?.principal) return; - dispatch(createAssetCanister({ userPrincipal: user.user.principal })); - }; - const sync = () => { - console.log("user AssetCanister", assetManager.userAssetCanister); - if (!user.user?.principal || !assetManager.userAssetCanister) return; - setSyncProgress({ - currentItem: "", - progress: 0, - totalSynced: 0, - currentProgress: 0, - }); - dispatch( - syncNfts({ - userPrincipal: user.user.principal, - syncProgress, - setSyncProgress, - userAssetCanister: assetManager.userAssetCanister, - }) - ); - console.log("syncing"); - }; - - useEffect(() => { - if (!assetManager.isLoading) { - console.log("getting caller asset canister"); - dispatch(getCallerAssetCanister()); - } - }, [user.user?.principal]); - - useEffect(() => { - setUserAssetCanister(assetManager.userAssetCanister); - if (assetManager.userAssetCanister) { - dispatch(getAssetList(assetManager.userAssetCanister)); - dispatch(getCanisterCycles(assetManager.userAssetCanister)); - sync(); - } - }, [assetManager.userAssetCanister]); - - return ( -
- {userAssetCanister === null ? ( -
- Asset Canister - -
- ) : ( -
- Asset Canister -

- - {assetManager.userAssetCanister} - -

- -

Cycles ≈ {assetManager.cycles}

-
- )} - - {syncProgress?.currentItem !== "" && ( -
-

- Synced :{syncProgress.totalSynced} -

- - {/* Current Item Being Processed */} -
- {syncProgress.currentProgress === 100 - ? "Upload Complete" - : `Uploading: ${syncProgress.currentItem}`} -
- - {/* Current NFT Upload Progress Bar */} -
-
-
- - {/* Total Synced Progress Bar */} -
- Total Synced: {syncProgress.totalSynced ?? 0} -
- -
-
-
-
- )} -
- ); -}; - -export default AssetManager; diff --git a/src/alex_frontend/core/apps/Modules/shared/components/NftDisplay/NftDisplay.tsx b/src/alex_frontend/core/apps/Modules/shared/components/NftDisplay/NftDisplay.tsx index da3f65ac1..12e5e8249 100644 --- a/src/alex_frontend/core/apps/Modules/shared/components/NftDisplay/NftDisplay.tsx +++ b/src/alex_frontend/core/apps/Modules/shared/components/NftDisplay/NftDisplay.tsx @@ -10,7 +10,6 @@ import { ContentService } from '@/apps/Modules/LibModules/contentDisplay/service import { useUsername } from '@/hooks/useUsername'; import { NftDisplayProps } from './types'; import { getTransactionService } from '@/apps/Modules/shared/services/transactionService'; -import { useAssetManager } from '@/hooks/actors'; // Constants const NFT_MANAGER_PRINCIPAL = "5sh5r-gyaaa-aaaap-qkmra-cai"; @@ -18,7 +17,7 @@ const MAX_FETCH_ATTEMPTS_PER_ARWEAVE_ID = 3; /** * Universal NFT Display Component - * + * * A flexible component for displaying NFTs consistently across the application. * Supports different display densities, data loading strategies, and customizable features. * Footer functionality has been removed and details are expected to be shown via hover effects. @@ -38,7 +37,6 @@ export const NftDisplay: React.FC }) => { const dispatch = useDispatch(); const store = useStore(); // Use useStore to get the store instance - const {actor} = useAssetManager(); // Component state const [isLoading, setIsLoading] = useState(true); const [transaction, setTransaction] = useState(providedTransaction); @@ -64,7 +62,6 @@ export const NftDisplay: React.FC const transactionDependency = providedTransaction ?? undefined; const loadNFTData = useCallback(async (mountedChecker: { isMounted: boolean }) => { - if(!actor) return; if (!tokenId) { if (mountedChecker.isMounted) { setError('Token ID is missing'); @@ -151,26 +148,26 @@ export const NftDisplay: React.FC // Directly mutate the ref's current value fetchAttemptsRef.current.set(currentArweaveId!, attempts + 1); - + console.log(`[NftDisplay] Transaction for Arweave ID ${currentArweaveId} not in Redux/props. Attempt ${attempts + 1}. Requesting fetch via TransactionService for token: ${tokenId}`); - + const transactionService = getTransactionService(dispatch, store.getState); try { // Service updates Redux. We rely on re-render from Redux state change. - await transactionService.fetchNftTransactions([currentArweaveId!], actor); // Added non-null assertion for currentArweaveId + await transactionService.fetchNftTransactions([currentArweaveId!]); // After this, the useEffect dependency on `transactionsFromRedux` should trigger a re-run. // For now, we just set loading and wait. - if (mountedChecker.isMounted) setIsLoading(true); + if (mountedChecker.isMounted) setIsLoading(true); return; // Exit and wait for Redux update } catch (serviceError) { console.error(`[NftDisplay] TransactionService failed for Arweave ID ${currentArweaveId} (Token: ${tokenId}):`, serviceError); throw new Error(`Service failed for ${currentArweaveId}: ${serviceError instanceof Error ? serviceError.message : String(serviceError)}`); } } - + if (!finalTransaction) { console.warn(`[NftDisplay] Transaction ${currentArweaveId} still not found after potential service call for token ${tokenId}. Might be transient or actual missing data.`); - if (mountedChecker.isMounted) setIsLoading(true); + if (mountedChecker.isMounted) setIsLoading(true); return; } @@ -183,13 +180,13 @@ export const NftDisplay: React.FC const content = await ContentService.loadContent(finalTransaction); finalContentUrls = await ContentService.getContentUrls(finalTransaction, content); if (mountedChecker.isMounted) { - dispatch(setContentData({ - id: finalTransaction.id, + dispatch(setContentData({ + id: finalTransaction.id, content: { ...content, urls: finalContentUrls } })); } } - + if (mountedChecker.isMounted) { setTransaction(finalTransaction); setContentUrls(finalContentUrls); @@ -200,10 +197,10 @@ export const NftDisplay: React.FC } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load NFT data'; - console.error(`[NftDisplay] loadNFTData error for token ${tokenId}:`, errorMessage, err); // Corrected template literal + console.error(`[NftDisplay] loadNFTData error for token ${tokenId}:`, errorMessage, err); if (mountedChecker.isMounted) { setError(errorMessage); - setFailedTokens(prev => new Set(prev).add(tokenId!)); + setFailedTokens(prev => new Set(prev).add(tokenId!)); } } finally { if (mountedChecker.isMounted) { @@ -216,14 +213,13 @@ export const NftDisplay: React.FC } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - actor, - tokenId, initialArweaveId, providedTransaction, //transactionDependency, // Using providedTransaction directly - dispatch, - store.getState, // Changed from store (full object) to store.getState (stable function reference) - nfts, arweaveToNftId, transactionsFromRedux, contentData, - failedTokens, // currentlyFetchingTokenIds, // This was causing issues, rely on isMounted and outer checks - loadingStrategy, isLoading + }, [ + tokenId, initialArweaveId, providedTransaction, + dispatch, + store.getState, + nfts, arweaveToNftId, transactionsFromRedux, contentData, + failedTokens, + loadingStrategy, isLoading ]); useEffect(() => { @@ -271,12 +267,12 @@ export const NftDisplay: React.FC console.warn(`[NftDisplay] Content rendering failed for token ${tokenId}. Marking as failed.`); setFailedTokens(prev => new Set(prev).add(tokenId)); setError("Content rendering failed for this NFT."); - setIsLoading(false); + setIsLoading(false); } }, [transaction, tokenId]); const handleClick = useCallback(() => { - if (failedTokens.has(tokenId!)) return; + if (failedTokens.has(tokenId!)) return; if (onClick) { onClick(); } else if (onViewDetails) { @@ -289,10 +285,10 @@ export const NftDisplay: React.FC }, [onClick, onViewDetails, tokenId, failedTokens]); // This is the section you added/modified, I will integrate the original logic here: - const displayErrorFromFailedTokens = failedTokens.has(tokenId!) - ? `Previously failed to load NFT: ${tokenId}. Data may not be available.` + const displayErrorFromFailedTokens = failedTokens.has(tokenId!) + ? `Previously failed to load NFT: ${tokenId}. Data may not be available.` : null; - + // Combine error from props/state with error from failed tokens const finalDisplayError = displayErrorFromFailedTokens || error; @@ -325,7 +321,7 @@ export const NftDisplay: React.FC // This can happen if transaction is present but ContentService is still fetching URLs. if (!transaction || !currentContent || !contentUrls) { // If arweaveId was provided and we are presumably waiting for parent, this state is more likely. - const waitingMessage = initialArweaveId && !transaction + const waitingMessage = initialArweaveId && !transaction ? `Preparing NFT data for ${tokenId}... (Waiting for transaction)` : `Preparing NFT content for ${tokenId}...`; return ( @@ -334,7 +330,7 @@ export const NftDisplay: React.FC
); } - + // Original renderContent function (inline or separate) const renderActualContent = () => { switch (variant) { @@ -378,16 +374,16 @@ export const NftDisplay: React.FC // Main return structure you provided, now with renderActualContent return ( -
-
- {renderActualContent()} + {renderActualContent()}
{/* Owner Information Display (from original) */} diff --git a/src/alex_frontend/core/apps/Modules/shared/services/transactionService.ts b/src/alex_frontend/core/apps/Modules/shared/services/transactionService.ts index 928d7ce07..1f4150891 100644 --- a/src/alex_frontend/core/apps/Modules/shared/services/transactionService.ts +++ b/src/alex_frontend/core/apps/Modules/shared/services/transactionService.ts @@ -18,10 +18,6 @@ import { setLoading, setError, } from "../state/transactions/transactionSlice"; -import { getAssetCanister } from "../state/assetManager/utlis"; -import { fetchAssetFromUserCanister } from "../state/assetManager/assetManagerThunks"; -import { ActorSubclass } from "@dfinity/agent"; -import { _SERVICE } from "../../../../../../declarations/asset_manager/asset_manager.did"; export class TransactionService { private dispatch: AppDispatch; @@ -32,7 +28,7 @@ export class TransactionService { this.getState = getState; } - async fetchNftTransactions(arweaveIds: string[], actor: ActorSubclass<_SERVICE>): Promise { + async fetchNftTransactions(arweaveIds: string[]): Promise { const operationStart = performance.now(); const arweaveIdsString = arweaveIds.join(","); console.log(`[BENCH] NFT_TX_FETCH_START: ${arweaveIds.length} IDs (${arweaveIdsString})`); @@ -41,9 +37,6 @@ export class TransactionService { this.dispatch(setError(null)); try { - const state = this.getState() as RootState; - const { selectedPrincipals } = state.library; - const arweaveFetchStart = performance.now(); console.log(`[BENCH] ARWEAVE_METADATA_FETCH_ALEXANDRIAN_START: ${arweaveIds.length} IDs (${arweaveIdsString})`); let transactions = await fetchTransactionsForAlexandrian(arweaveIds); @@ -56,55 +49,8 @@ export class TransactionService { throw new Error("No Arweave metadata found for the NFTs."); } - const userAssetCanisterPrincipal = selectedPrincipals[0]; - if (userAssetCanisterPrincipal && userAssetCanisterPrincipal !== 'new') { - console.log(`[TransactionService] Checking user ${userAssetCanisterPrincipal}'s asset canister for ${transactions.length} transactions.`); - const icpCheckOverallStart = performance.now(); - try { - const userAssetCanisterId = await getAssetCanister(userAssetCanisterPrincipal, actor); - - if (userAssetCanisterId) { - console.log(`[TransactionService] User ${userAssetCanisterPrincipal} has asset canister ${userAssetCanisterId}. Fetching assets...`); - - const icpAssetFetchAllStart = performance.now(); - let assetsFoundOnICP = 0; - const assetFetchPromises = transactions.map(async (transaction) => { - try { - const result = await fetchAssetFromUserCanister(transaction.id, userAssetCanisterId); - - if (result?.blob) { - assetsFoundOnICP++; - const assetUrl = URL.createObjectURL(result.blob); - console.log(`[TransactionService] SUCCESS: Asset ${transaction.id} found in user canister ${userAssetCanisterId}.`); - return { ...transaction, assetUrl }; - } else { - return transaction; - } - } catch (individualAssetError) { - const errorMessage = individualAssetError instanceof Error && individualAssetError.message.includes("asset not found") - ? "Asset explicitly not found by canister" - : String(individualAssetError); - console.warn( - `[TransactionService] ERROR fetching asset ${transaction.id} from user canister ${userAssetCanisterId}: ${errorMessage}` - ); - return transaction; - } - }); - transactions = await Promise.all(assetFetchPromises); - console.log(`[BENCH] ICP_ASSET_FETCH_ALL_ATTEMPTED: User ${userAssetCanisterPrincipal}, Canister ${userAssetCanisterId} - ${assetsFoundOnICP}/${transactions.length} found - ${(performance.now() - icpAssetFetchAllStart).toFixed(2)}ms`); - } else { - console.log(`[TransactionService] User ${userAssetCanisterPrincipal} has no assigned asset canister. Skipping ICP asset check.`); - } - } catch (setupError) { - console.error(`[TransactionService] Error during asset canister setup for user ${userAssetCanisterPrincipal}:`, setupError); - } - console.log(`[BENCH] ICP_CANISTER_CHECK_DURATION: User ${userAssetCanisterPrincipal} - ${(performance.now() - icpCheckOverallStart).toFixed(2)}ms`); - } else { - console.log("[TransactionService] No specific user principal for asset canister check, or principal is 'new'. Skipping ICP asset check."); - } - this.dispatch(setTransactions(transactions)); - + const loadContentStart = performance.now(); console.log(`[BENCH] CONTENT_PREPARATION_START: ${transactions.length} txs (NFT flow)`); await this.loadContentForTransactions(transactions); @@ -148,10 +94,10 @@ export class TransactionService { const permasearchFetchEnd = performance.now(); const permasearchStatus = transactions && transactions.length > 0 ? 'success' : 'not_found_or_empty'; console.log(`[BENCH] ARWEAVE_METADATA_FETCH_PERMASEARCH_END: ${logIdentifier} - ${permasearchStatus} - ${(permasearchFetchEnd - permasearchFetchStart).toFixed(2)}ms`); - + if (transactions && transactions.length > 0) { this.dispatch(setTransactions(transactions)); - + const loadContentStart = performance.now(); console.log(`[BENCH] CONTENT_PREPARATION_START: ${transactions.length} txs (Permasearch flow)`); await this.loadContentForTransactions(transactions); @@ -194,14 +140,13 @@ export class TransactionService { async loadContentForTransactions(transactions: Transaction[]): Promise { await Promise.all( transactions.map(async (transaction) => { - const sourceForDataItem = transaction.assetUrl ? 'ic_canister' : 'arweave'; try { const contentMetadata = await ContentService.loadContent(transaction); const urls = await ContentService.getContentUrls(transaction, contentMetadata); this.dispatch( setContentData({ id: transaction.id, - content: { ...contentMetadata, urls, source: sourceForDataItem }, + content: { ...contentMetadata, urls }, }) ); } catch (error) { diff --git a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerSlice.ts b/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerSlice.ts deleted file mode 100644 index ae6999c87..000000000 --- a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerSlice.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - ActionReducerMapBuilder, - createSlice, - PayloadAction, -} from "@reduxjs/toolkit"; -import { - createAssetCanister, - fetchUserNfts, - getCallerAssetCanister, - getAssetList, - syncNfts, - getCanisterCycles, -} from "./assetManagerThunks"; -import { toast } from "sonner"; - -interface AssetManagerState { - isLoading: boolean; - error: string | null; - userAssetCanister: string | null; - assetList: Array<{ key: string; content_type: string }>; - urls: string[]; - cycles:string; -} - -const initialState: AssetManagerState = { - isLoading: false, - error: "", - urls: [""], - assetList: [{ key: "", content_type: "" }], - userAssetCanister: null, - cycles:"" -}; - -const assetManagerSlice = createSlice({ - name: "assetManager", - initialState, - reducers: { - setIsLoading: (state, action: PayloadAction) => { - state.isLoading = action.payload; - }, - }, - extraReducers: (builder: ActionReducerMapBuilder) => { - builder - .addCase(createAssetCanister.pending, (state) => { - state.isLoading = true; - state.error = null; - }) - .addCase(createAssetCanister.fulfilled, (state, action) => { - state.isLoading = false; - state.userAssetCanister = action.payload; - state.error = null; - toast.success("New asset canister created!"); - }) - .addCase(createAssetCanister.rejected, (state, action) => { - state.isLoading = false; - state.error = action.payload as string; - toast.error(action.payload as string); - }) - .addCase(getCallerAssetCanister.pending, (state) => { - state.error = null; - }) - .addCase(getCallerAssetCanister.fulfilled, (state, action) => { - // state.isLoading = false; - state.userAssetCanister = action.payload; - state.error = null; - }) - .addCase(getCallerAssetCanister.rejected, (state, action) => { - // state.isLoading = false; - state.error = action.payload as string; - // toast.error(action.payload as string); - }) - .addCase(fetchUserNfts.pending, (state) => { - state.isLoading = true; - state.error = null; - }) - .addCase(fetchUserNfts.fulfilled, (state, action) => { - state.isLoading = false; - state.urls = action.payload; - state.error = null; - }) - .addCase(fetchUserNfts.rejected, (state, action) => { - state.isLoading = false; - state.error = action.payload as string; - // toast.error(action.payload as string); - }) - .addCase(getAssetList.pending, (state) => { - state.error = null; - }) - .addCase(getAssetList.fulfilled, (state, action) => { - state.assetList = action.payload; - state.error = null; - }) - .addCase(getAssetList.rejected, (state, action) => { - state.error = action.payload as string; - // toast.error(action.payload as string); - }) - .addCase(syncNfts.pending, (state) => { - state.isLoading = true; - state.error = null; - }) - .addCase(syncNfts.fulfilled, (state, action) => { - state.isLoading = false; - state.error = null; - }) - .addCase(syncNfts.rejected, (state, action) => { - state.isLoading = false; - state.error = action.payload as string; - // toast.error(action.payload as string); - }) - .addCase(getCanisterCycles.pending, (state) => { - state.error = null; - }) - .addCase(getCanisterCycles.fulfilled, (state, action) => { - state.cycles=action.payload; - state.error = null; - }) - .addCase(getCanisterCycles.rejected, (state, action) => { - state.error = action.payload as string; - toast.error(action.payload as string); - }); - }, -}); - -export const { setIsLoading } = assetManagerSlice.actions; - -export default assetManagerSlice.reducer; diff --git a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerThunks.ts b/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerThunks.ts deleted file mode 100644 index f81608587..000000000 --- a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/assetManagerThunks.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { - getActorAssetManager, - getLbryActor, - getIcrc7Actor, - getActorUserAssetCanister, -} from "@/features/auth/utils/authUtils"; -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { Principal } from "@dfinity/principal"; -import { natToArweaveId } from "@/utils/id_convert"; -import { uploadAsset } from "./uploadToAssetCanister"; -import { fetchTransactionsForAlexandrian } from "@/apps/Modules/LibModules/arweaveSearch/api/arweaveApi"; -import { RootState } from "@/store"; -import { createTokenAdapter } from "@/apps/Modules/shared/adapters/TokenAdapter"; - -export const createAssetCanister = createAsyncThunk< - string, // Success type - { userPrincipal: string }, - { rejectValue: string } ->( - "assetManager/createAssetCanister", - async ({ userPrincipal }, { rejectWithValue }) => { - try { - const actor = await getActorAssetManager(); - const assetManagerCanisterId = process.env.CANISTER_ID_ASSET_MANAGER!; - const actorLbryLedger = await getLbryActor(); - let amountFormatApprove: bigint = BigInt( - Number((Number(10) + 0.04) * 10 ** 8).toFixed(0) - ); - const checkApproval = await actorLbryLedger.icrc2_allowance({ - account: { - owner: Principal.fromText(userPrincipal), - subaccount: [], - }, - spender: { - owner: Principal.fromText(assetManagerCanisterId), - subaccount: [], - }, - }); - - if (checkApproval.allowance < amountFormatApprove) { - const resultIcpApprove = await actorLbryLedger.icrc2_approve({ - spender: { - owner: Principal.fromText(assetManagerCanisterId), - subaccount: [], - }, - amount: amountFormatApprove, - fee: [], - memo: [], - from_subaccount: [], - created_at_time: [], - expected_allowance: [], - expires_at: [], - }); - if ("Err" in resultIcpApprove) { - const error = resultIcpApprove.Err; - let errorMessage = "Unknown error"; // Default error message - if ("TemporarilyUnavailable" in error) { - errorMessage = "Service is temporarily unavailable"; - } - throw new Error(errorMessage); - } - } - - const result = await actor.create_asset_canister(); - - if ("Ok" in result) { - return result.Ok.toString(); - } - if ("Err" in result) { - return rejectWithValue(result.Err.toString()); - } - - return rejectWithValue("Unexpected response format"); - } catch (error) { - console.error("Error creating asset canister:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } - } -); - -export const getCallerAssetCanister = createAsyncThunk< - string, - void, - { rejectValue: string } ->("assetManager/getCallerAssetCanister", async (_, { rejectWithValue }) => { - try { - const actor = await getActorAssetManager(); - const result = await actor.get_caller_asset_canister(); - if (result[0]) { - const canisterId = result[0]?.assigned_canister_id; - if (!canisterId) { - return rejectWithValue("No canister ID found"); - } - return canisterId.toString(); - } else { - return rejectWithValue("No canister ID found"); - } - } catch (error) { - console.error("Error fetching asset canister:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } -}); - -export interface syncProgressInterface { - currentItem: string; - progress: number; - totalSynced: number; - currentProgress: number; - // attempt?: number; -} - -export const syncNfts = createAsyncThunk< - string, - { - userPrincipal: string; - userAssetCanister: string; - setSyncProgress: React.Dispatch< - React.SetStateAction - >; - syncProgress: syncProgressInterface; - }, - { rejectValue: string } ->( - "assetManager/syncNfts", - async ( - { userPrincipal, userAssetCanister, setSyncProgress, syncProgress }, - { dispatch, getState, rejectWithValue } - ) => { - try { - const nftAdapter = createTokenAdapter("NFT"); - - const result = await nftAdapter.getTokensOf( - Principal.fromText(userPrincipal), - undefined, - BigInt(10000) - ); - - const tokens: string[] = []; - for (const tokenId of result) { - const nftData = await nftAdapter.tokenToNFTData(tokenId, userPrincipal); - tokens.push(nftData.arweaveId); - } - - const fetchedTransactions = JSON.stringify( - await fetchTransactionsForAlexandrian(tokens) - ); - - const state = getState() as RootState; - const assetManager = state.assetManager; - const assetCanisterId = assetManager.userAssetCanister; - - const transactionUploadResult = await uploadAsset({ - assetCanisterId: assetCanisterId || "", - id: "ContentData", - setSyncProgress, - syncProgress, - contentData: fetchedTransactions, - assetList: assetManager.assetList, - }); - - if (!transactionUploadResult) { - throw new Error("Failed to upload transaction data."); - } - - tokens.reduce(async (prevPromise, token) => { - await prevPromise; - const result = await uploadAsset({ - assetCanisterId: assetCanisterId || "", - itemUrl: "https://arweave.net/" + token, - id: token, - setSyncProgress, - syncProgress, - assetList: assetManager.assetList, - }); - }, Promise.resolve()); - - return ""; - } catch (error) { - console.error("Error fetching NFTs:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } - } -); - -export const fetchUserNfts = createAsyncThunk< - string[], - { - userPrincipal: string; - userAssetCanister: string; - }, - { rejectValue: string } ->( - "assetManager/fetchNfts", - async ({ userPrincipal, userAssetCanister }, { rejectWithValue }) => { - try { - const actorIcrc7 = await getIcrc7Actor(); - - const countLimit = [BigInt(10000)] as [bigint]; - - const result = await actorIcrc7.icrc7_tokens_of( - { - owner: Principal.fromText(userPrincipal), - subaccount: [], - }, - [], - countLimit - ); - - if (!Array.isArray(result) || result.length === 0) { - console.warn("No tokens found for the specified user."); - } - const tokens = result.map((value) => natToArweaveId(value)); - console.log("tokens are ", tokens); - - const urls = await Promise.all( - tokens.map(async (id) => { - const assetResult = await fetchAssetFromUserCanister(id, userAssetCanister); - return assetResult?.blob ? URL.createObjectURL(assetResult.blob) : ""; - }) - ); - - return urls; - } catch (error) { - console.error("Error fetching NFTs:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } - } -); - -export const getAssetList = createAsyncThunk< - Array<{ key: string; content_type: string }>, - string, - { rejectValue: string } ->("assetManager/getAssetList", async (canisterId, { rejectWithValue }) => { - if (!canisterId) { - return rejectWithValue("No canister ID found"); - } - - try { - const assetActor = await getActorUserAssetCanister(canisterId); - const result = await assetActor.list({}); - - if (!result || !Array.isArray(result.entries)) { - return rejectWithValue("Invalid response from asset canister"); - } - - const simplifiedList = Array.from(result.entries()).map(([key, value]) => ({ - key: value.key, - content_type: value.content_type || "unknown", - })); - return simplifiedList; - } catch (error) { - console.error("Error fetching asset canister:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } -}); - -export const getCanisterCycles = createAsyncThunk< - string, - string, - { rejectValue: string } ->("assetManager/getCanisterCycles", async (canisterId, { rejectWithValue }) => { - if (!canisterId) { - return rejectWithValue("No canister ID found"); - } - - try { - const actor = await getActorAssetManager(); - - const result = await actor.get_canister_cycles(Principal.fromText(canisterId)); - if ("Ok" in result) { - return result.Ok.toString(); - } - if ("Err" in result) { - return rejectWithValue(result.Err.toString()); - } - - return rejectWithValue("Unexpected response format"); - } - catch (error) { - console.error("Error fetching asset canister:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } -}); - -export const fetchAssetFromUserCanister = async ( - arweaveId: string, - canisterId: string -): Promise<{ blob: Blob; contentType: string } | null> => { - const url = `https://${canisterId}.raw.ic0.app/arweave/${arweaveId}`; - console.log(`[assetManagerThunks] fetchAssetFromUserCanister: Attempting to fetch from URL: "${url}"`); - - const icpFetchStart = performance.now(); - let responseStatus = 0; - - try { - const networkStart = performance.now(); - const response = await fetch(url); - const networkEnd = performance.now(); - responseStatus = response.status; - console.log(`[BENCH] ICP_NETWORK_FETCH: ID ${arweaveId} from Canister ${canisterId} - Status ${response.status} - ${(networkEnd - networkStart).toFixed(2)}ms`); - - if (response.ok) { - const processStart = performance.now(); - const blob = await response.blob(); - const processEnd = performance.now(); - console.log(`[BENCH] ICP_BLOB_PROCESSING: ID ${arweaveId} from Canister ${canisterId} - ${(processEnd - processStart).toFixed(2)}ms`); - const contentType = response.headers.get("Content-Type") || "application/octet-stream"; - console.log(`[assetManagerThunks] fetchAssetFromUserCanister: Successfully fetched. Blob Type: ${blob.type}, Size: ${blob.size}, Content-Type: ${contentType}`); - const icpFetchEnd = performance.now(); - console.log(`[BENCH] ICP_FETCH_TOTAL: ID ${arweaveId} from Canister ${canisterId} - Status ${response.status} - Success - ${(icpFetchEnd - icpFetchStart).toFixed(2)}ms`); - return { blob, contentType }; - } else { - console.warn(`[assetManagerThunks] fetchAssetFromUserCanister: Failed to fetch. Status: ${response.status} ${response.statusText}`); - const icpFetchEnd = performance.now(); - console.log(`[BENCH] ICP_FETCH_TOTAL: ID ${arweaveId} from Canister ${canisterId} - Status ${response.status} - Failed - ${(icpFetchEnd - icpFetchStart).toFixed(2)}ms`); - return null; - } - } catch (error) { - console.error(`[assetManagerThunks] fetchAssetFromUserCanister: Network or other error fetching from URL "${url}":`, error); - const icpFetchEnd = performance.now(); - console.log(`[BENCH] ICP_FETCH_TOTAL: ID ${arweaveId} from Canister ${canisterId} - Status ${responseStatus === 0 ? 'network_error' : responseStatus} - Error - ${(icpFetchEnd - icpFetchStart).toFixed(2)}ms`); - return null; - } -}; diff --git a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/uploadToAssetCanister.tsx b/src/alex_frontend/core/apps/Modules/shared/state/assetManager/uploadToAssetCanister.tsx deleted file mode 100644 index 7566238c3..000000000 --- a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/uploadToAssetCanister.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { getActorUserAssetCanister } from "@/features/auth/utils/authUtils"; -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { toast } from 'sonner'; -import { syncProgressInterface } from "./assetManagerThunks"; -import { RootState } from "@/store"; - -interface CreateBatchResponse { - batch_id: bigint; -} - -interface CreateChunksResponse { - chunk_ids: bigint[]; -} - -type HeaderField = [string, string][]; - -interface AssetCanister { - create_batch: (args: {}) => Promise; - create_chunks: (args: { - batch_id: bigint; - content: Uint8Array[]; - }) => Promise; - create_asset: (args: { - key: string; - content_type: string; - headers: [] | [HeaderField[]]; - allow_raw_access: boolean[]; - max_age: bigint[]; - enable_aliasing: boolean[]; - }) => Promise; - commit_batch: (args: { - batch_id: bigint; - operations: Array<{ - SetAssetContent: { - key: string; - sha256: Uint8Array[]; - chunk_ids: bigint[]; - content_encoding: string; - }; - }>; - }) => Promise; - delete_batch: (args: { batch_id: bigint }) => Promise; - delete_asset: (args: { key: string }) => Promise; -} - -interface MediaState { - blob: Blob | null; - contentType: string | null; -} -interface uploadProps { - assetCanisterId: string, - itemUrl?: string, - contentData?: string; - id: string, - syncProgress: syncProgressInterface, - setSyncProgress: React.Dispatch>, - assetList: Array<{ key: string; content_type: string }>; - - // setUploadProgress: React.Dispatch> - - -} -// Constants for upload configuration -const UPLOAD_CONSTANTS = { - MAX_CHUNKS_PER_BATCH: 5, // Reduced from 10 to 5 - CHUNK_SIZE: 512 * 1024, // Reduced to 512KB per chunk - MAX_RETRIES: 3, - RETRY_DELAY: 2000, - BACKOFF_FACTOR: 1.5, -}; - - - -// Add retry logic helper -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -async function retryOperation( - operation: () => Promise, - retryCount: number = 0 -): Promise { - try { - return await operation(); - } catch (error: any) { - if (retryCount >= UPLOAD_CONSTANTS.MAX_RETRIES) { - throw error; - } - const delay = UPLOAD_CONSTANTS.RETRY_DELAY * - Math.pow(UPLOAD_CONSTANTS.BACKOFF_FACTOR, retryCount); - console.log(`Retry attempt ${retryCount + 1} after ${delay}ms`); - await sleep(delay); - return retryOperation(operation, retryCount + 1); - } -} -const calculateSHA256 = async (data: Uint8Array): Promise => { - const hashBuffer = await crypto.subtle.digest("SHA-256", data as Uint8Array); - return new Uint8Array(hashBuffer); -}; - -export const uploadAsset = async ({ - assetCanisterId, - id, - syncProgress, - setSyncProgress, - itemUrl, - contentData, - assetList -}: uploadProps): Promise => { - let currentBatchId: bigint | null = null; - const assetActor = await getActorUserAssetCanister(assetCanisterId); - - try { - setSyncProgress((prev) => ({ ...prev, currentItem: id, currentProgress: 5 })); - - let fileData: Uint8Array; - let contentType = "application/json"; // Default to JSON if uploading content data - - if (itemUrl) { - // NFT Upload Case - - if (assetList.some(asset => asset.key === id)) { - setSyncProgress((prev) => ({ - ...prev, - totalSynced: (prev.totalSynced || 0) + 1, - currentProgress: 100 - })); - return true; // no need to upload again - } - - const response = await fetch(itemUrl); - if (!response.ok) throw new Error(`Failed to fetch file: ${response.statusText}`); - - contentType = response.headers.get("content-type") || "image/jpeg"; - const blob = await response.blob(); - fileData = new Uint8Array(await blob.arrayBuffer()); - } else if (contentData) { - // Check if ContentData exists in the asset list - const assetList = await assetActor.list({}); - const contentDataExists = assetList.some(asset => asset.key === "ContentData"); - - // If ContentData exists, delete it first - if (contentDataExists) { - await assetActor.delete_asset({ key: "ContentData" }); - console.log("Existing ContentData deleted successfully"); - } - - // Transaction JSON Upload Case - fileData = new TextEncoder().encode(contentData); - } else { - throw new Error("Either `itemUrl` or `contentData` must be provided."); - } - - const totalChunks = Math.ceil(fileData.length / UPLOAD_CONSTANTS.CHUNK_SIZE); - - setSyncProgress((prev) => ({ ...prev, currentProgress: 10 })); - - // Step 2: Create the asset - const headers: [string, string][] = [ - ["Content-Type", contentType], - ["Accept-Ranges", "bytes"], - ["Cache-Control", "public, max-age=3600"], - ]; - - await retryOperation(async () => { - await assetActor.create_asset({ - key: id, - content_type: contentType, - headers: [headers], - allow_raw_access: [true], - max_age: [BigInt(3600)], - enable_aliasing: [true], - }); - }); - - setSyncProgress((prev) => ({ ...prev, currentProgress: 15 })); - - // Step 3: Create batch - const createBatchResponse = await retryOperation(async () => - assetActor.create_batch({}) - ); - currentBatchId = createBatchResponse.batch_id; - - // Step 4: Upload chunks - let allChunkIds: bigint[] = []; - for (let batchStart = 0; batchStart < totalChunks; batchStart += UPLOAD_CONSTANTS.MAX_CHUNKS_PER_BATCH) { - const batchEnd = Math.min(batchStart + UPLOAD_CONSTANTS.MAX_CHUNKS_PER_BATCH, totalChunks); - const currentBatchChunks = Array.from({ length: batchEnd - batchStart }, (_, i) => { - const start = (batchStart + i) * UPLOAD_CONSTANTS.CHUNK_SIZE; - const end = Math.min(start + UPLOAD_CONSTANTS.CHUNK_SIZE, fileData.length); - return fileData.slice(start, end); - }); - - setSyncProgress((prev) => ({ - ...prev, - currentProgress: Math.round((batchStart / totalChunks) * 70) + 20 - })); - - const createChunksResponse = await retryOperation(async () => - assetActor.create_chunks({ - batch_id: currentBatchId!, - content: currentBatchChunks, - }) - ); - - allChunkIds = [...allChunkIds, ...createChunksResponse.chunk_ids]; - await sleep(500); - } - - // Step 5: Calculate hash and commit - setSyncProgress((prev) => ({ ...prev, currentProgress: 90 })); - - const sha256 = await calculateSHA256(fileData); - - await retryOperation(async () => { - if (!currentBatchId) throw new Error("Batch ID is null"); - - await assetActor.commit_batch({ - batch_id: currentBatchId, - operations: [ - { - SetAssetContent: { - key: id, - sha256: [sha256], - chunk_ids: allChunkIds, - content_encoding: "gzip", - }, - }, - ], - }); - }); - - setSyncProgress((prev) => ({ - ...prev, - totalSynced: (prev.totalSynced || 0) + 1, - currentProgress: 100 - })); - console.log(`Upload of "${id}" completed successfully.`); - - // toast.success(`Upload of "${id}" completed successfully.`); - return true; - - } catch (error: any) { - console.error("Upload failed:", error); - // toast.error("Upload failed: " + (error.message || "Unknown error")); - - // Cleanup on failure - if (currentBatchId && assetActor) { - try { - await assetActor.delete_batch({ batch_id: currentBatchId }); - await assetActor.delete_asset({ key: id }); - } catch (cleanupError) { - console.error("Cleanup failed:", cleanupError); - } - } - - return false; - } -}; diff --git a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/utlis.ts b/src/alex_frontend/core/apps/Modules/shared/state/assetManager/utlis.ts deleted file mode 100644 index 3c68a0c64..000000000 --- a/src/alex_frontend/core/apps/Modules/shared/state/assetManager/utlis.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ActorSubclass } from "@dfinity/agent"; -import { _SERVICE } from "../../../../../../../declarations/asset_manager/asset_manager.did"; - -export const getAssetCanister = async (principal: string, actor: ActorSubclass<_SERVICE>): Promise => { - try { - const result =await actor.get_all_user_asset_canisters(); - - const matchingCanister = result - .map(([userPrincipal, canisterRegistry]) => { - if (canisterRegistry.owner.toString() === principal) { - return canisterRegistry.assigned_canister_id.toString(); - } - return null; - }) - .find((canisterId) => canisterId !== null); // Get the first non-null value - - return matchingCanister || null; - } catch (error) { - console.error("Error fetching asset canister:", error); - return null; - } - }; - \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/shared/state/librarySearch/libraryThunks.ts b/src/alex_frontend/core/apps/Modules/shared/state/librarySearch/libraryThunks.ts index 1fe543353..b1cf97274 100644 --- a/src/alex_frontend/core/apps/Modules/shared/state/librarySearch/libraryThunks.ts +++ b/src/alex_frontend/core/apps/Modules/shared/state/librarySearch/libraryThunks.ts @@ -1,6 +1,5 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { togglePrincipal, setLoading, setSearchParams, updateLastSearchTimestamp, setTotalItems, setCollection } from './librarySlice'; -import { updateTransactions } from '@/apps/Modules/shared/state/transactions/transactionThunks'; import { RootState } from '@/store'; import { toggleSortDirection } from './librarySlice'; import { AppDispatch } from '@/store'; @@ -8,8 +7,6 @@ import { fetchTokensForPrincipal, FetchTokensParams } from '../nftData/nftDataTh import { clearNfts } from '../nftData/nftDataSlice'; import { Principal } from '@dfinity/principal'; import { createTokenAdapter, TokenType } from '../../adapters/TokenAdapter'; -import { ActorSubclass } from '@dfinity/agent'; -import { _SERVICE } from '../../../../../../../declarations/asset_manager/asset_manager.did'; const DEBOUNCE_TIME = 300; // ms const DEFAULT_PAGE_SIZE = 20; @@ -25,19 +22,19 @@ export const togglePrincipalSelection = createAsyncThunk< // Get current state const currentState = getState(); const currentPrincipals = currentState.library.selectedPrincipals; - + // Only clear NFTs if we're changing principals (not just re-selecting) const isNewPrincipalSelection = !currentPrincipals.includes(principalId); if (isNewPrincipalSelection) { dispatch(clearNfts()); } - + dispatch(togglePrincipal(principalId)); // Get current collection type const state = getState(); const collection = state.library.collection; - + // Create the appropriate token adapter const tokenAdapter = createTokenAdapter(collection as TokenType); @@ -55,7 +52,7 @@ export const togglePrincipalSelection = createAsyncThunk< // Reset search params to start from the beginning const pageSize = state.library.searchParams.pageSize; - dispatch(setSearchParams({ + dispatch(setSearchParams({ start: 0, end: Math.min(pageSize, Number(totalCount)), pageSize @@ -71,15 +68,15 @@ export const togglePrincipalSelection = createAsyncThunk< export const performSearch = createAsyncThunk< void, - { actor: ActorSubclass<_SERVICE> }, + void, { state: RootState; dispatch: AppDispatch } >( 'library/performSearch', - async ({actor}, { getState, dispatch }) => { + async (_, { getState, dispatch }) => { const state = getState(); const now = Date.now(); const timeSinceLastSearch = now - state.library.lastSearchTimestamp; - + if (timeSinceLastSearch < DEBOUNCE_TIME) { return; } @@ -87,30 +84,29 @@ export const performSearch = createAsyncThunk< // Check if we're adding more content to existing results const currentTransactions = state.transactions.transactions; const isInitialSearch = currentTransactions.length === 0; - + // Only clear NFTs if this is an initial search or if explicitly requested // This prevents wiping out NFT data when loading content after initial fetch if (isInitialSearch) { dispatch(clearNfts()); } - + dispatch(updateLastSearchTimestamp(now)); dispatch(setLoading(true)); - + try { const { selectedPrincipals, collection, searchParams, totalItems } = state.library; const pageSize = searchParams.pageSize || DEFAULT_PAGE_SIZE; if (selectedPrincipals && selectedPrincipals.length > 0 && collection) { const params: FetchTokensParams = { - actor, principalId: selectedPrincipals[0], collection, page: 1, itemsPerPage: pageSize, startFromEnd: searchParams.startFromEnd, totalItems // Pass through the total items for proper pagination - + }; if (searchParams.start !== undefined) { @@ -118,15 +114,15 @@ export const performSearch = createAsyncThunk< } //We fetched the transaction, loadcontent and balance here const result = await dispatch(fetchTokensForPrincipal(params)).unwrap(); - + // If we need to update the total count if (totalItems === undefined || totalItems === 0) { const collectionType = collection; // Create the appropriate token adapter const tokenAdapter = createTokenAdapter(collectionType as TokenType); - + let totalCount: bigint; - + if (selectedPrincipals.length === 0 || selectedPrincipals[0] === 'new') { // For 'new' option or when no principal is selected, get total supply totalCount = await tokenAdapter.getTotalSupply(); @@ -136,24 +132,10 @@ export const performSearch = createAsyncThunk< const principal = Principal.fromText(principalId); totalCount = await tokenAdapter.getBalanceOf(principal); } - + // Update total items in the store dispatch(setTotalItems(Number(totalCount))); } - - const currentState = getState(); - // Don't update totalItems here since we want to preserve the actual total from the contract - - const arweaveIds = Object.values(currentState.nftData.nfts) - .filter(nft => - nft.principal === selectedPrincipals[0] && - nft.collection === collection - ) - .map(nft => nft.arweaveId); - - const uniqueArweaveIds = [...new Set(arweaveIds)] as string[]; - // why are we fetching nft content again here we already did in fetchTokensForPrincipal, should keep one of them ? - //await dispatch(updateTransactions(uniqueArweaveIds)); } } catch (error) { console.error('Search failed:', error); @@ -188,14 +170,14 @@ export const changeCollection = createAsyncThunk< try { // Clear existing NFTs when changing collection dispatch(clearNfts()); - + // Update the collection type dispatch(setCollection(collectionType)); - + // Get current state after collection update const state = getState(); const selectedPrincipal = state.library.selectedPrincipals[0] || 'new'; - + // Create the appropriate token adapter for the new collection const tokenAdapter = createTokenAdapter(collectionType as TokenType); @@ -213,12 +195,12 @@ export const changeCollection = createAsyncThunk< // Reset search params to appropriate values based on the new total const pageSize = state.library.searchParams.pageSize; - dispatch(setSearchParams({ + dispatch(setSearchParams({ start: 0, end: Math.min(pageSize, Number(totalCount)), pageSize })); - + // Don't automatically trigger a search - let the user do it explicitly } catch (error) { console.error('Error in changeCollection:', error); @@ -227,8 +209,8 @@ export const changeCollection = createAsyncThunk< } ); -export const toggleSort = (actor: ActorSubclass<_SERVICE>) => (dispatch: AppDispatch) => { +export const toggleSort = () => (dispatch: AppDispatch) => { dispatch(clearNfts()); dispatch(toggleSortDirection()); - dispatch(performSearch({actor})); -}; \ No newline at end of file + dispatch(performSearch()); +}; diff --git a/src/alex_frontend/core/apps/Modules/shared/state/nftData/nftDataThunks.ts b/src/alex_frontend/core/apps/Modules/shared/state/nftData/nftDataThunks.ts index 403e1666f..276e5db7c 100644 --- a/src/alex_frontend/core/apps/Modules/shared/state/nftData/nftDataThunks.ts +++ b/src/alex_frontend/core/apps/Modules/shared/state/nftData/nftDataThunks.ts @@ -21,8 +21,6 @@ import { TokenAdapter, } from "../../adapters/TokenAdapter"; import { perpetua } from "../../../../../../../declarations/perpetua"; -import { ActorSubclass } from "@dfinity/agent"; -import { _SERVICE } from "../../../../../../../declarations/asset_manager/asset_manager.did"; const NFT_MANAGER_PRINCIPAL = "5sh5r-gyaaa-aaaap-qkmra-cai"; @@ -161,7 +159,6 @@ const fetchNFTBatchHelper = async (params: BatchFetchParams[]) => { // Export the interface so it can be imported by other files export interface FetchTokensParams { - actor: ActorSubclass<_SERVICE>; principalId: string; collection: "NFT" | "SBT"; page: number; @@ -178,7 +175,6 @@ export const fetchTokensForPrincipal = createAsyncThunk< "nftData/fetchTokensForPrincipal", async ( { - actor, principalId, collection, page, @@ -487,7 +483,7 @@ export const fetchTokensForPrincipal = createAsyncThunk< // load loadContentForTransactions in child await dispatch( - fetchNftTransactions({arweaveIds, actor}) as unknown as AnyAction + fetchNftTransactions({arweaveIds}) as unknown as AnyAction ).unwrap(); // If we're using the 'new' option, make sure all tokens have owner information diff --git a/src/alex_frontend/core/apps/Modules/shared/state/transactions/transactionThunks.ts b/src/alex_frontend/core/apps/Modules/shared/state/transactions/transactionThunks.ts index ea2ada76c..35a50fcfb 100644 --- a/src/alex_frontend/core/apps/Modules/shared/state/transactions/transactionThunks.ts +++ b/src/alex_frontend/core/apps/Modules/shared/state/transactions/transactionThunks.ts @@ -7,24 +7,19 @@ import { AppDispatch, RootState } from "@/store"; import { Transaction } from "../../../shared/types/queries"; import { fetchTransactionsForAlexandrian } from "@/apps/Modules/LibModules/arweaveSearch/api/arweaveApi"; import { setTransactions } from "./transactionSlice"; -import { getAssetCanister } from "../assetManager/utlis"; -import { getActorUserAssetCanister } from "@/features/auth/utils/authUtils"; -import { fetchAssetFromUserCanister } from "../assetManager/assetManagerThunks"; -import { ActorSubclass } from "@dfinity/agent"; -import { _SERVICE } from "../../../../../../../declarations/asset_manager/asset_manager.did"; /** * Fetch transactions for NFTs */ export const fetchNftTransactions = createAsyncThunk< Transaction[], - {arweaveIds: string[], actor: ActorSubclass<_SERVICE>}, + {arweaveIds: string[]}, { dispatch: AppDispatch; state: RootState } >( "transactions/fetchNftTransactions", - async ({arweaveIds, actor}, { dispatch, getState }) => { + async ({arweaveIds}, { dispatch, getState }) => { const transactionService = getTransactionService(dispatch, getState); - return await transactionService.fetchNftTransactions(arweaveIds, actor); + return await transactionService.fetchNftTransactions(arweaveIds); } ); @@ -107,16 +102,14 @@ export const removeTransaction = createAsyncThunk< */ export const updateTransactions = createAsyncThunk< Transaction[], - {arweaveIds: string[], actor: ActorSubclass<_SERVICE>}, + {arweaveIds: string[]}, { dispatch: AppDispatch; state: RootState } >( "transactions/updateTransactions", - async ({arweaveIds, actor}, { dispatch, getState }) => { + async ({arweaveIds}, { dispatch, getState }) => { const state = getState() as RootState; - const { selectedPrincipals } = state.library; const existingTransactions = state.transactions.transactions; const nfts = state.nftData?.nfts || {}; - let userAssetCanisterd = await getAssetCanister(selectedPrincipals[0], actor); if (arweaveIds.length === 0) { return existingTransactions; @@ -173,63 +166,8 @@ export const updateTransactions = createAsyncThunk< ...otherTransactions, ]; - if (userAssetCanisterd) { - const getContentData = await fetchAssetFromUserCanister( - "ContentData", - userAssetCanisterd - ); - - if (getContentData?.blob) { - try { - const blobData = await getContentData.blob.arrayBuffer(); - const textData = new TextDecoder().decode(blobData); - const jsonData = JSON.parse(textData); - - let transactions = Array.isArray(jsonData) ? jsonData : [jsonData]; - transactions = transactions.filter((tx) => - arweaveIds.includes(tx.id) - ); - - const fetchPromises = transactions.map(async (transaction) => { - try { - const result = await fetchAssetFromUserCanister( - transaction.id, - userAssetCanisterd - ); - - const assetUrl = result?.blob - ? URL.createObjectURL(result.blob) - : ""; - - return { id: transaction.id, assetUrl }; - } catch (error) { - console.error( - `Failed to fetch asset for transaction ${transaction.id}:`, - error - ); - return { id: transaction.id, assetUrl: "" }; - } - }); - - const assetResults = await Promise.all(fetchPromises); - - requestedTransactions = requestedTransactions.map((tx) => { - const found = assetResults.find((a) => a.id === tx.id); - return found ? { ...tx, assetUrl: found.assetUrl } : tx; - }); - } catch (error) { - console.error("Failed to process data:", error); - } - } else { - console.warn("No data found in ContentData."); - } - } - console.log("Final Transactions:", requestedTransactions); - // dispatch(setTransactions(sortedMergedTransactions)); - - dispatch(setTransactions([])); // is not effecting the code sill displaying nfts - + dispatch(setTransactions([])); const transactionsToLoad = newTransactions.filter( (newTx) => diff --git a/src/alex_frontend/core/apps/app/Perpetua/features/cards/hooks/useNftData.ts b/src/alex_frontend/core/apps/app/Perpetua/features/cards/hooks/useNftData.ts index 505399a74..d806f4f9b 100644 --- a/src/alex_frontend/core/apps/app/Perpetua/features/cards/hooks/useNftData.ts +++ b/src/alex_frontend/core/apps/app/Perpetua/features/cards/hooks/useNftData.ts @@ -27,7 +27,6 @@ export const useNftData = (tokenId: string | undefined) => { const dispatch = useDispatch(); // --- Selectors --- - const { canisters: allUserAssetCanistersMap, canisterLoading: authCanisterMapLoading } = useSelector((state: RootState) => state.auth); const nftStaticDataFromCache = useSelector((state: RootState) => tokenId ? state.nftData.nfts[tokenId] : null); const arweaveTxFromCache = useSelector((state: RootState) => { if (!tokenId) return null; @@ -50,7 +49,7 @@ export const useNftData = (tokenId: string | undefined) => { const [ownerPrincipal, setOwnerPrincipal] = useState(null); const [derivedArweaveId, setDerivedArweaveId] = useState(null); const [assetContentUrls, setAssetContentUrls] = useState(null); - const [assetSource, setAssetSource] = useState<'ic_canister' | 'arweave' | 'unknown' | null>(null); + const [assetSource, setAssetSource] = useState<'arweave' | 'unknown' | null>(null); const [currentArweaveTx, setCurrentArweaveTx] = useState(null); @@ -179,162 +178,8 @@ export const useNftData = (tokenId: string | undefined) => { } - // 3. Attempt to load from ICP Asset Canister - let assetLoadedFromICP = false; - if (currentOwner && localDerivedArweaveId && allUserAssetCanistersMap) { - const ownerText = currentOwner.toText(); - const userAssetCanisterIdString = allUserAssetCanistersMap[ownerText]; - console.log(`[useNftData ${tokenId}] Owner: ${ownerText}, Asset canisters available: ${Object.keys(allUserAssetCanistersMap).length}, Has canister: ${!!userAssetCanisterIdString}`); - - if (userAssetCanisterIdString) { - console.log(`[useNftData ${tokenId}] User ${ownerText} has asset canister ${userAssetCanisterIdString}. Attempting ICP load for /arweave/${localDerivedArweaveId}`); - const icpLoadAttemptStart = performance.now(); - - // Helper function to attempt ICP fetch with timeout and retries - const fetchWithRetry = async (url: string, maxRetries = 2, timeoutMs = 10000): Promise => { - for (let attempt = 1; attempt <= maxRetries; attempt++) { - console.log(`[useNftData ${tokenId}] ICP fetch attempt ${attempt}/${maxRetries}: ${url}`); - - let timeoutId: NodeJS.Timeout | undefined; - - try { - const controller = new AbortController(); - timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - const response = await fetch(url, { - signal: controller.signal, - cache: 'no-cache', // Prevent aggressive caching that might cause inconsistencies - headers: { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache' - } - }); - - clearTimeout(timeoutId); - console.log(`[useNftData ${tokenId}] ICP fetch attempt ${attempt} response: ${response.status} ${response.statusText}`); - - if (response.ok) { - return response; - } else if (response.status === 404) { - // Don't retry 404s - asset doesn't exist - throw new Error(`Asset not found (404): ${url}`); - } else if (attempt === maxRetries) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - // For other errors (5xx, network issues), continue to retry - console.warn(`[useNftData ${tokenId}] ICP fetch attempt ${attempt} failed with ${response.status}, retrying...`); - - } catch (error: any) { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (error.name === 'AbortError') { - console.warn(`[useNftData ${tokenId}] ICP fetch attempt ${attempt} timed out after ${timeoutMs}ms`); - } else if (error.message.includes('404')) { - throw error; // Don't retry 404s - } - - if (attempt === maxRetries) { - throw error; - } - - console.warn(`[useNftData ${tokenId}] ICP fetch attempt ${attempt} error:`, error.message, '- retrying...'); - // Brief delay before retry - await new Promise(resolve => setTimeout(resolve, 500 * attempt)); - } - } - throw new Error('All retry attempts failed'); - }; - - try { - // Use the same URL construction approach as useInit.ts - const isLocal = process.env.DFX_NETWORK == "local"; - const baseUrl = isLocal ? `http://${userAssetCanisterIdString}.localhost:4943` : `https://${userAssetCanisterIdString}.raw.icp0.io`; - const canisterAssetUrl = `${baseUrl}/arweave/${localDerivedArweaveId}`; - - console.log(`[useNftData ${tokenId}] Attempting to fetch: ${canisterAssetUrl}`); - const response = await fetchWithRetry(canisterAssetUrl); - - if (!mounted) { - console.log(`[useNftData ${tokenId}] Component unmounted during ICP fetch, aborting`); - return; - } - - const contentType = response.headers.get('Content-Type') ?? undefined; - const contentLength = response.headers.get('Content-Length'); - console.log(`[useNftData ${tokenId}] ICP response headers - Content-Type: ${contentType}, Content-Length: ${contentLength}`); - - const blob = await response.blob(); - console.log(`[useNftData ${tokenId}] ICP blob created - size: ${blob.size} bytes, type: ${blob.type}`); - - if (blob.size === 0) { - throw new Error('Received empty blob from ICP canister'); - } - - const objectUrl = URL.createObjectURL(blob); - console.log(`[useNftData ${tokenId}] ICP object URL created: ${objectUrl}`); - - const icpUrls: ContentUrlInfo = { - thumbnailUrl: null, - coverUrl: null, - fullUrl: objectUrl, - }; - setAssetContentUrls(icpUrls); - setAssetSource('ic_canister'); - - // Create a minimal transaction object for ICP-loaded content - const icpTransaction: ArweaveTransaction = { - id: localDerivedArweaveId, - owner: currentOwner.toText(), - tags: [] - }; - setCurrentArweaveTx(icpTransaction); - - // For text content, extract the text - let textContent = null; - if (contentType?.includes('text/') || contentType?.includes('application/json')) { - textContent = await blob.text(); - console.log(`[useNftData ${tokenId}] ICP text content extracted - length: ${textContent?.length || 0} chars`); - } - - const contentDataItem: ContentDataItem = { - url: canisterAssetUrl, - textContent: textContent, - imageObjectUrl: objectUrl, - thumbnailUrl: null, - error: null, - data: blob, - source: 'ic_canister', - contentType: contentType, - urls: icpUrls - }; - dispatch(setContentData({ id: localDerivedArweaveId, content: contentDataItem })); - console.log(`[useNftData ${tokenId}] SUCCESS: Asset ${localDerivedArweaveId} loaded from ICP canister ${userAssetCanisterIdString} (${contentType}, ${blob.size} bytes).`); - assetLoadedFromICP = true; - - if (mounted) { - setIsAssetLoading(false); - setIsNftDetailsLoading(false); - setError(null); - } - - } catch (icpError: any) { - console.error(`[useNftData ${tokenId}] FAILED: Error fetching asset ${localDerivedArweaveId} from ICP canister ${userAssetCanisterIdString}:`, { - error: icpError.message, - stack: icpError.stack, - name: icpError.name - }); - // Don't set error state here - let it fall back to Arweave - } - console.log(`[BENCH] ICP_LOAD_ATTEMPT: ${localDerivedArweaveId} for token ${tokenId} - ${assetLoadedFromICP ? 'success' : 'failed_or_not_found'} - ${(performance.now() - icpLoadAttemptStart).toFixed(2)}ms`); - } else { - console.log(`[useNftData ${tokenId}] No asset canister found for owner ${ownerText}. Skipping ICP asset check.`); - } - } - - - // 4. Fallback to Arweave if not loaded from ICP - if (!assetLoadedFromICP && localDerivedArweaveId && mounted) { + // 3. Load from Arweave + if (localDerivedArweaveId && mounted) { console.log(`[useNftData ${tokenId}] Proceeding to Arweave fallback for ${localDerivedArweaveId}.`); const arweaveFallbackStart = performance.now(); if (mounted) setAssetSource('arweave'); @@ -421,13 +266,9 @@ export const useNftData = (tokenId: string | undefined) => { setIsNftDetailsLoading(false); } console.log(`[BENCH] ARWEAVE_FALLBACK_PROCESSING: Token ${tokenId} - ${(performance.now() - arweaveFallbackStart).toFixed(2)}ms`); - } else if (assetLoadedFromICP && mounted) { - setIsAssetLoading(false); - setIsNftDetailsLoading(false); } - - // 5. Load Balances + // 4. Load Balances const needsBalanceFetch = currentOwner && (!nftStaticDataFromCache?.balances || (nftStaticDataFromCache.balances.alex === '0' && nftStaticDataFromCache.balances.lbry === '0')); if (needsBalanceFetch && currentOwner && localDerivedArweaveId) { if (mounted) setIsBalanceLoading(true); diff --git a/src/alex_frontend/core/components/CanisterCard/CanisterCardSkeleton.tsx b/src/alex_frontend/core/components/CanisterCard/CanisterCardSkeleton.tsx deleted file mode 100644 index df9933a5c..000000000 --- a/src/alex_frontend/core/components/CanisterCard/CanisterCardSkeleton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; - -function CanisterCardSkeleton() { - return ( -
- {/* Header skeleton */} -
-
-
- -
-
- {/* Icon skeleton */} -
-
-
- - {/* Text skeleton - using multiple lines for paragraph */} -
-
-
-
- - {/* Button skeleton */} -
-
-
-
- ) -} - -export default CanisterCardSkeleton; \ No newline at end of file diff --git a/src/alex_frontend/core/components/CanisterCard/CanisterView.tsx b/src/alex_frontend/core/components/CanisterCard/CanisterView.tsx deleted file mode 100644 index 53ca52406..000000000 --- a/src/alex_frontend/core/components/CanisterCard/CanisterView.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useNavigate } from "@tanstack/react-router"; -import { Check } from "lucide-react"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { Button } from "@/lib/components/button"; -import Copy from "../Copy"; -import { useAssetManager } from "@/hooks/actors"; -import { Principal } from "@dfinity/principal"; - -function CanisterView() { - const { actor } = useAssetManager(); - const navigate = useNavigate(); - - const [cycles, setCycles] = useState('N/A'); - const { canister } = useAppSelector(state => state.auth); - - // fetch cycles - - const fetchCycles = useCallback(async () => { - if(!canister || !actor) return; - - try { - const result = await actor.get_canister_cycles(Principal.fromText(canister)); - if ("Ok" in result) { - setCycles(result.Ok.toString()); - } - if ("Err" in result) { - throw new Error("Failed to fetch cycles"); - } - } catch (error) { - console.error("Error fetching cycles:", error); - setCycles("N/A"); - } - }, [canister, actor]); - - useEffect(() => { - fetchCycles(); - }, [canister, fetchCycles]); - - if(!canister) return null; - - return ( -
-
- -
- - Your canister has been created. - - {cycles && ( - - It has {cycles} cycles remaining. - - )} - -
- {canister} - -
- -
- ) -} - -export default CanisterView; \ No newline at end of file diff --git a/src/alex_frontend/core/components/CanisterCard/NonCanisterView.tsx b/src/alex_frontend/core/components/CanisterCard/NonCanisterView.tsx deleted file mode 100644 index 3785e99a8..000000000 --- a/src/alex_frontend/core/components/CanisterCard/NonCanisterView.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; -import { Button } from "@/lib/components/button"; -import { LockKeyhole } from "lucide-react"; -import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { createCanister } from "@/features/auth/thunks/createCanister"; -import { useAssetManager, useLbry } from "@/hooks/actors"; -import { toast } from "sonner"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/lib/components/alert-dialog"; -// import { createAssetCanister } from "@/apps/Modules/shared/state/assetManager/assetManagerThunks"; - -function NonCanisterView() { - const {actor: assetManagerActor} = useAssetManager(); - const {actor: lbryActor} = useLbry(); - - const dispatch = useAppDispatch(); - const {user, canisterError} = useAppSelector(state=> state.auth) - - const handleCreate = ()=>{ - try{ - if(!user) throw new Error('Unauthenticated User'); - if(!assetManagerActor) throw new Error('Asset Manager Actor not available'); - if(!lbryActor) throw new Error('LBRY Actor not available'); - - // dispatch(createAssetCanister({ userPrincipal: user.principal })) - dispatch(createCanister({assetManagerActor, lbryActor})) - }catch(error){ - console.log('create error,' , error); - toast.error('Failed. ' + (error instanceof Error ? error.message : String(error))) - } - if(!user) return; - } - - return ( -
-
- -
-
- - You do not have a canister yet. - - - Create a canister to start uploading assets. - -
- - - - - - - Create Your Own Asset Canister? - - This will cost 500 LBRY (approximately $5). - -
-

Benefits of having your own asset canister include:

-
    -
  • - It will be yours forever.
  • -
  • - Assets will load much faster.
  • -
  • - Enhanced in-app visibility and other perks.
  • -
-
-
- - Cancel - - Confirm & Create - - -
-
- {canisterError && -
- - An Error Occured while creating canister. - - {canisterError} -
- } -
- ); -} - -NonCanisterView.displayName = 'NonCanisterView'; - -export default NonCanisterView; \ No newline at end of file diff --git a/src/alex_frontend/core/components/CanisterCard/index.tsx b/src/alex_frontend/core/components/CanisterCard/index.tsx deleted file mode 100644 index ff31082e2..000000000 --- a/src/alex_frontend/core/components/CanisterCard/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import CanisterView from "./CanisterView"; -import NonCanisterView from "./NonCanisterView"; -import CanisterCardSkeleton from "./CanisterCardSkeleton"; - -function CanisterCard() { - // const dispatch = useAppDispatch(); - const { canister, canisterLoading, canisterError } = useAppSelector( - (state) => state.auth - ); - - if (canisterLoading) { - return ; - } - - return ( -
-
-
Canister
-
-
- {canister ? : } -
-
- ); -} - -export default CanisterCard; diff --git a/src/alex_frontend/core/components/NftProvider/index.tsx b/src/alex_frontend/core/components/NftProvider/index.tsx index 20b03f717..4fdc246dd 100644 --- a/src/alex_frontend/core/components/NftProvider/index.tsx +++ b/src/alex_frontend/core/components/NftProvider/index.tsx @@ -14,7 +14,7 @@ interface NftContextType { setModal: (value: Modal | null) => void; } -const NftContext = createContext(undefined); +export const NftContext = createContext(undefined); export const useNftContext = () => { const context = useContext(NftContext); diff --git a/src/alex_frontend/core/features/arweave-assets/arweaveAssetsSlice.ts b/src/alex_frontend/core/features/arweave-assets/arweaveAssetsSlice.ts index 0c803ce72..ea25c6353 100644 --- a/src/alex_frontend/core/features/arweave-assets/arweaveAssetsSlice.ts +++ b/src/alex_frontend/core/features/arweave-assets/arweaveAssetsSlice.ts @@ -1,23 +1,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { fetchUserArweaveAssets } from "./thunks/fetchUserArweaveAssets"; -import { deleteAssetFromCanister } from "./thunks/deleteAssetFromCanister"; -import { checkAssetsAvailability } from "./thunks/checkAssetsAvailability"; -import { checkAssetAvailability } from "./thunks/checkAssetAvailability"; -import { pullAssetToCanister } from "./thunks/pullAssetToCanister"; import { ArweaveAssetItem, ArweaveAssetsState } from "./types"; -import { pullAllAssets } from "./thunks/pullAllAssets"; const initialState: ArweaveAssetsState = { assets: [], selected: null, - pulling: null, - pullError: null, - - deleting: null, - deleteError: null, - loading: false, error: null, }; @@ -26,9 +15,6 @@ const assetsSlice = createSlice({ name: "arweaveAssets", initialState, reducers: { - setPulling: (state, action: PayloadAction) => { - state.pulling = action.payload; - }, setAssets: (state, action: PayloadAction) => { state.assets = action.payload; }, @@ -53,77 +39,9 @@ const assetsSlice = createSlice({ .addCase(fetchUserArweaveAssets.rejected, (state, action) => { state.loading = false; state.error = action.payload || "Failed to fetch assets"; - }) - - .addCase(deleteAssetFromCanister.pending, (state, action) => { - state.deleting = action.meta.arg.asset.id; - state.deleteError = null; - }) - .addCase(deleteAssetFromCanister.fulfilled, (state, action) => { - state.deleting = null; - state.deleteError = null; - }) - .addCase(deleteAssetFromCanister.rejected, (state, action) => { - state.deleting = null; - state.deleteError = action.payload as string; - }) - - // .addCase(checkAssetsAvailability.pending, (state) => { - // state.loading = true; - // state.error = null; - // }) - // .addCase(checkAssetsAvailability.fulfilled, (state, action) => { - // state.loading = false; - // state.assets = action.payload; - // }) - // .addCase(checkAssetsAvailability.rejected, (state, action) => { - // state.loading = false; - // state.error = action.payload || "Failed to check assets availability"; - // }) - - // .addCase(checkAssetAvailability.pending, (state, action) => { - // state.loading = true; - // state.error = null; - // }) - // .addCase(checkAssetAvailability.fulfilled, (state, action) => { - // state.loading = false; - // state.error = null; - // state.assets = state.assets.map(asset => - // asset.id === action.payload.id ? action.payload : asset - // ); - // }) - // .addCase(checkAssetAvailability.rejected, (state, action) => { - // state.loading = false; - // state.error = action.payload as string; - // }) - - .addCase(pullAssetToCanister.pending, (state, action) => { - state.pulling = action.meta.arg.asset.id; - state.pullError = null; - }) - .addCase(pullAssetToCanister.fulfilled, (state, action) => { - state.pulling = null; - state.pullError = null; - }) - .addCase(pullAssetToCanister.rejected, (state, action) => { - state.pulling = null; - state.pullError = action.payload as string; - }) - - .addCase(pullAllAssets.pending, (state, action) => { - state.pulling = null; - state.pullError = null; - }) - .addCase(pullAllAssets.fulfilled, (state, action) => { - state.pulling = null; - state.pullError = null; - }) - .addCase(pullAllAssets.rejected, (state, action) => { - state.pulling = null; - state.pullError = action.payload as string; }); }, }); -export const { selectAsset, clearAssets, setAssets, setPulling } = assetsSlice.actions; +export const { selectAsset, clearAssets, setAssets } = assetsSlice.actions; export default assetsSlice.reducer; diff --git a/src/alex_frontend/core/features/arweave-assets/components/AssetDetail.tsx b/src/alex_frontend/core/features/arweave-assets/components/AssetDetail.tsx index 3ffb0062e..0c435485b 100644 --- a/src/alex_frontend/core/features/arweave-assets/components/AssetDetail.tsx +++ b/src/alex_frontend/core/features/arweave-assets/components/AssetDetail.tsx @@ -1,7 +1,5 @@ -import React, { useEffect } from "react"; +import React from "react"; import { ArweaveAssetItem } from "../types"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { toast } from "sonner"; import { Button } from "@/lib/components/button"; import { Dialog, @@ -10,63 +8,18 @@ import { DialogHeader, DialogTitle, } from "@/lib/components/dialog"; -import { Clock, Download, ExternalLink, FileType, Trash2, X } from "lucide-react"; +import { Clock, Download, ExternalLink, FileType, X } from "lucide-react"; import { getFileTypeInfo, getFileTypeName } from "@/features/pinax/constants"; import Copy from "@/components/Copy"; import { selectAsset } from "../arweaveAssetsSlice"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { checkAssetAvailability } from "../thunks/checkAssetAvailability"; -import { pullAssetToCanister } from "../thunks/pullAssetToCanister"; -import { deleteAssetFromCanister } from "../thunks/deleteAssetFromCanister"; -import { AssetManager } from "@dfinity/assets"; - -const isLocal = process.env.DFX_NETWORK == "local"; interface AssetDetailProps { asset: ArweaveAssetItem; - assetManager: AssetManager | null; } -const AssetDetail: React.FC = ({ asset, assetManager }) => { +const AssetDetail: React.FC = ({ asset }) => { const dispatch = useAppDispatch(); - const { canister } = useAppSelector(state => state.auth); - const {pulling, pullError, deleting, deleteError} = useAppSelector((state) => state.arweaveAssets); - const { assets: icpAssets } = useAppSelector((state) => state.icpAssets); - - // Check if asset is in canister - useEffect(() => { - if(assetManager){ - dispatch(checkAssetAvailability({ asset, assetManager })); - } - - }, [assetManager, asset]); - - // Function to pull asset to user's canister - const handlePullAsset = async () => { - if (!assetManager) { - toast.error("No asset canister available. Please create one first."); - return; - } - - dispatch(pullAssetToCanister({ asset, assetManager })); - }; - - // Function to delete asset from user's canister - const handleDeleteAsset = async () => { - if (!assetManager) { - toast.error("No asset canister available."); - return; - } - - // Confirm deletion - if (!window.confirm( - `Are you sure you want to delete this asset from your canister?\nThis won't delete it from Arweave.` - )) { - return; - } - - dispatch(deleteAssetFromCanister({ asset, assetManager })); - }; // Helper function to format timestamp const formatDate = (timestamp?: number) => { @@ -100,26 +53,10 @@ const AssetDetail: React.FC = ({ asset, assetManager }) => { return "bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-900/40 dark:text-gray-300 dark:border-gray-800"; }; - // Generate canister asset URL - const getCanisterAssetUrl = () => { - if (!canister) return ""; - const baseUrl = isLocal - ? `http://${canister}.localhost:4943` - // : `https://${canister}.ic0.app`; - : `https://${canister}.raw.icp0.io`; - return `${baseUrl}/arweave/${asset.id}`; - }; - - const canisterAssetUrl = getCanisterAssetUrl(); - const handleClose = () => { dispatch(selectAsset(null)); }; - const isAvailableInCanister = (asset: ArweaveAssetItem) => { - return icpAssets.find((icpAsset) => icpAsset.key === `/arweave/${asset.id}`) ? true : false; - } - return ( handleClose()}> @@ -156,14 +93,14 @@ const AssetDetail: React.FC = ({ asset, assetManager }) => { {isImage && (

{fileTypeName} File

-
@@ -234,25 +171,6 @@ const AssetDetail: React.FC = ({ asset, assetManager }) => { )}
- {/* Divider before Canister URL section */} -
- - {/* Canister URL section */} - {isAvailableInCanister(asset) && canisterAssetUrl && ( -
-

- - Canister URL -

-
-
- {canisterAssetUrl} -
- -
-
- )} -

@@ -291,40 +209,6 @@ const AssetDetail: React.FC = ({ asset, assetManager }) => {

- -
  • -
    - {pulling === asset.id ? ( -
    - ) : ( -
    - {isAvailableInCanister(asset) ? ( -
    - ) : ( -
    - )} -
    - )} -
    -
    - - Storage - - {pulling === asset.id ? ( - - Checking... - - ) : isAvailableInCanister(asset) ? ( - - Available in your canister - - ) : ( - - Arweave only - - )} -
    -
  • - + {asset.tags && asset.tags.length > 0 && (

    @@ -388,86 +254,6 @@ const AssetDetail: React.FC = ({ asset, assetManager }) => { )}

    - - {/* Fixed footer for actions */} -
    - {canister && !isAvailableInCanister(asset) && ( -
    -
    - -
    -
    -

    - Speed Up Access -

    -

    - Pull to your canister for faster loading -

    -
    - -
    - )} - - {canister && isAvailableInCanister(asset) && ( -
    -
    -
    - - - -
    -
    -

    - Canister Storage Active -

    -

    - Stored in your canister for faster access -

    -
    -
    - -
    - )} -
    ); diff --git a/src/alex_frontend/core/features/arweave-assets/components/AssetTable.tsx b/src/alex_frontend/core/features/arweave-assets/components/AssetTable.tsx index 6a031122b..7344ad8c5 100644 --- a/src/alex_frontend/core/features/arweave-assets/components/AssetTable.tsx +++ b/src/alex_frontend/core/features/arweave-assets/components/AssetTable.tsx @@ -1,17 +1,10 @@ import React, { useEffect } from "react"; import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { toast } from "sonner"; import { Button } from "@/lib/components/button"; import { - Check, - Cloud, - CloudOff, - Download, - Ellipsis, ExternalLink, Eye, } from "lucide-react"; -import { Link } from "@tanstack/react-router"; import { Table, TableBody, @@ -20,41 +13,21 @@ import { TableHeader, TableRow, } from "@/lib/components/table"; -import { Alert } from "@/components/Alert"; import { formatFileSize } from "@/features/pinax/utils"; import { getFileTypeInfo, getFileTypeName } from "@/features/pinax/constants"; import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { pullAssetToCanister } from "../thunks/pullAssetToCanister"; import { selectAsset } from "../arweaveAssetsSlice"; import { fetchUserArweaveAssets } from "../thunks/fetchUserArweaveAssets"; -import { AssetManager } from "@dfinity/assets"; import { ArweaveAssetItem } from "../types"; -interface AssetTableProps { - assetManager: AssetManager | null; -} - -const AssetTable: React.FC = ({ assetManager }) => { +const AssetTable: React.FC = () => { const dispatch = useAppDispatch(); - const { assets: arweaveAssets, loading, pulling, error, selected } = useAppSelector(state => state.arweaveAssets); - const { assets: icpAssets } = useAppSelector((state) => state.icpAssets); - - const { canister } = useAppSelector((state) => state.auth); + const { assets: arweaveAssets, loading } = useAppSelector(state => state.arweaveAssets); useEffect(() => { dispatch(fetchUserArweaveAssets()); }, []); - // Function to pull asset to user's canister - const handlePullAsset = async (asset: ArweaveAssetItem) => { - if (!assetManager) { - toast.error("No asset canister available. Please create one first."); - return; - } - - dispatch(pullAssetToCanister({ asset, assetManager })); - }; - if (loading) { return (
    @@ -93,14 +66,10 @@ const AssetTable: React.FC = ({ assetManager }) => { if (!contentType) return "Unknown"; const fileTypeInfo = getFileTypeInfo(contentType); const typeName = getFileTypeName(contentType); - + return fileTypeInfo ? `${typeName}` : typeName; }; - const isAvailableInCanister = (asset: ArweaveAssetItem) => { - return icpAssets.find((icpAsset) => icpAsset.key === `/arweave/${asset.id}`) ? true : false; - } - return (
    @@ -112,7 +81,6 @@ const AssetTable: React.FC = ({ assetManager }) => { Type Size Created - Storage Actions @@ -161,17 +129,6 @@ const AssetTable: React.FC = ({ assetManager }) => { {formatDate(asset.timestamp)} - - {!canister ? ( - - ) : pulling === asset.id ? ( - - ) : isAvailableInCanister(asset) ? ( - - ) : ( - - )} -
    - - {canister && !isAvailableInCanister(asset) && ( - - )} - - {!canister && ( - - )}
    diff --git a/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetAvailability.ts b/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetAvailability.ts deleted file mode 100644 index 86e7a8254..000000000 --- a/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetAvailability.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { ArweaveAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; - -// Thunk for checking availability of a single asset -export const checkAssetAvailability = createAsyncThunk< - ArweaveAssetItem, - { - asset: ArweaveAssetItem, - assetManager: AssetManager, - }, - { rejectValue: string } ->("assets/checkAssetAvailability", async ({asset, assetManager}, { rejectWithValue }) => { - try { - const assetKey = `/arweave/${asset.id}`; - const canisterAsset = await assetManager.get(assetKey); - - return { - ...asset, - pulled: canisterAsset !== undefined - }; - } catch (error) { - console.error("Error checking asset availability:", error); - return { - ...asset, - pulled: false - }; - } -}); \ No newline at end of file diff --git a/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetsAvailability.ts b/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetsAvailability.ts deleted file mode 100644 index c8b9921df..000000000 --- a/src/alex_frontend/core/features/arweave-assets/thunks/checkAssetsAvailability.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { ArweaveAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; - -export const checkAssetsAvailability = createAsyncThunk< - ArweaveAssetItem[], - { - assets: ArweaveAssetItem[], - assetManager: AssetManager, - }, - { rejectValue: string } ->("assets/checkAssetsAvailability", async ({assets, assetManager}, { rejectWithValue }) => { - try { - const canisterAssets = await assetManager.list(); - - const assetKeysInCanister = new Set(canisterAssets.map((asset) => asset.key)); - - // Update assets with their pulled status - const updatedAssets = assets.map(asset => ({ - ...asset, - pulled: assetKeysInCanister.has(`/arweave/${asset.id}`) - })); - - return updatedAssets; - } catch (error) { - console.error("Error checking assets availability:", error); - return rejectWithValue(error instanceof Error ? error.message : "Unknown error occurred"); - } -}); diff --git a/src/alex_frontend/core/features/arweave-assets/thunks/deleteAssetFromCanister.ts b/src/alex_frontend/core/features/arweave-assets/thunks/deleteAssetFromCanister.ts deleted file mode 100644 index afed97c20..000000000 --- a/src/alex_frontend/core/features/arweave-assets/thunks/deleteAssetFromCanister.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { ArweaveAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; - -export const deleteAssetFromCanister = createAsyncThunk< - boolean, - { - asset: ArweaveAssetItem, - assetManager: AssetManager, - }, - { rejectValue: string } ->("assets/deleteFromCanister", async ({asset, assetManager}, { rejectWithValue }) => { - try { - const assetKey = `/arweave/${asset.id}`; - const batch = assetManager.batch(); - batch.delete(assetKey); - await batch.commit(); - return true; - } catch (error) { - console.error("Error deleting asset from canister:", error); - return rejectWithValue(error instanceof Error ? error.message : "Unknown error occurred"); - } -}); diff --git a/src/alex_frontend/core/features/arweave-assets/thunks/pullAllAssets.ts b/src/alex_frontend/core/features/arweave-assets/thunks/pullAllAssets.ts deleted file mode 100644 index a1d06d6ba..000000000 --- a/src/alex_frontend/core/features/arweave-assets/thunks/pullAllAssets.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { AssetManager } from "@dfinity/assets"; -import { fetchFile } from "../utils/assetUtils"; -import { uploadToCanister } from "../utils/assetUtils"; -import { AppDispatch, RootState } from "@/store"; -import { setPulling } from "../arweaveAssetsSlice"; -import { addAsset } from "@/features/icp-assets/icpAssetsSlice"; - -export const pullAllAssets = createAsyncThunk< - void, - { - assetManager: AssetManager, - }, - { state: RootState, rejectValue: string, dispatch: AppDispatch } ->("assets/pullAllAssets", async ({ assetManager }, { getState, rejectWithValue, dispatch }) => { - try { - const assets = getState().arweaveAssets.assets; - const icpAssets = getState().icpAssets.assets; - - const assetsToPull = assets.filter(asset => !icpAssets.some(icpAsset => icpAsset.key === `/arweave/${asset.id}`)); - - for (const asset of assetsToPull) { - dispatch(setPulling(asset.id)); - try{ - // Fetch the file using our utility function - const file = await fetchFile(asset); - - // Upload the file using our utility function - await uploadToCanister(assetManager, "/arweave", file, asset.id); - dispatch(addAsset({ - key: `/arweave/${asset.id}`, - encodings: [], - content_type: file.type - })) - } catch (error) { - console.error(`Error details for asset ${asset.id}:`, error); - if (error instanceof Error && error.message.includes("is out of cycles")) { - throw new Error(`Aborting!! Canister is out of cycles. Please top up the canister.`); - } - - if (error instanceof Error && error.message.includes("already exists")) { - throw new Error(`Aborting!! Asset ${asset.id} already exists in the canister.`); - } - - throw new Error(`Aborting!! Failed to pull asset ${asset.id}`); - }finally{ - dispatch(setPulling(null)); - } - } - } catch (error) { - console.error("Failed to pull assets:", error); - return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); - } -}); \ No newline at end of file diff --git a/src/alex_frontend/core/features/arweave-assets/thunks/pullAssetToCanister.ts b/src/alex_frontend/core/features/arweave-assets/thunks/pullAssetToCanister.ts deleted file mode 100644 index 7f508f490..000000000 --- a/src/alex_frontend/core/features/arweave-assets/thunks/pullAssetToCanister.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { ArweaveAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; -import { fetchFile } from "../utils/assetUtils"; -import { uploadToCanister } from "../utils/assetUtils"; - -export const pullAssetToCanister = createAsyncThunk< - ArweaveAssetItem, - { - asset: ArweaveAssetItem, - assetManager: AssetManager, - }, - { rejectValue: string } ->("assets/pullToCanister", async ({ asset, assetManager }, { rejectWithValue }) => { - try { - // Fetch the file using our utility function - const file = await fetchFile(asset); - - // Upload the file using our utility function - await uploadToCanister(assetManager, "/arweave", file, asset.id); - - // Return updated asset with pulled status - return { - ...asset, - pulled: true - }; - } catch (error) { - console.error("Failed to pull asset to canister:", error); - return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); - } -}); \ No newline at end of file diff --git a/src/alex_frontend/core/features/arweave-assets/types.ts b/src/alex_frontend/core/features/arweave-assets/types.ts index c159f2753..db566b96e 100644 --- a/src/alex_frontend/core/features/arweave-assets/types.ts +++ b/src/alex_frontend/core/features/arweave-assets/types.ts @@ -13,12 +13,6 @@ export interface ArweaveAssetsState { selected: ArweaveAssetItem | null; - pulling: string | null; - pullError: string | null; - - deleting: string | null; - deleteError: string | null; - loading: boolean; error: string | null; } diff --git a/src/alex_frontend/core/features/arweave-assets/utils/assetUtils.ts b/src/alex_frontend/core/features/arweave-assets/utils/assetUtils.ts deleted file mode 100644 index 94ed185ad..000000000 --- a/src/alex_frontend/core/features/arweave-assets/utils/assetUtils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AssetManager } from "@dfinity/assets"; -import { ArweaveAssetItem } from "../types"; - -/** - * Fetches a file from Arweave or provided URL - * @param asset The asset to fetch - * @returns Promise resolving to a File object - */ -export async function fetchFile(asset: ArweaveAssetItem): Promise { - // Use asset URL or construct one from the asset ID - const assetUrl = asset.url || `https://arweave.net/${asset.id}`; - - // Fetch the file - const response = await fetch(assetUrl); - - // If the response is not ok, throw an error - if (!response.ok) throw new Error(`Failed to fetch asset: ${response.statusText}`); - - // Get the blob data - const blob = await response.blob(); - - // Determine content type - use asset's contentType, response header, or default - const contentType = asset.contentType || response.headers.get("Content-Type") || "application/octet-stream"; - - // Create and return a File object - return new File([blob], `${asset.id}`, { type: contentType }); -} - -/** - * Uploads a file to the user's canister - * @param assetManager The asset manager instance - * @param path The path to upload the file to - * @param file The file to upload - * @param fileName The name of the file - * @returns Promise resolving when upload is complete - */ -export async function uploadToCanister(assetManager: AssetManager, path: string = "/uploads", file: File, fileName: string): Promise { - // Create a batch - const batch = assetManager.batch(); - - // Store the file with a path that includes arweave/ prefix - await batch.store(file, { path, fileName }); - - // Commit the batch - await batch.commit(); -} diff --git a/src/alex_frontend/core/features/auth/authExtraReducers.ts b/src/alex_frontend/core/features/auth/authExtraReducers.ts index 52bd3fe19..d3d084056 100644 --- a/src/alex_frontend/core/features/auth/authExtraReducers.ts +++ b/src/alex_frontend/core/features/auth/authExtraReducers.ts @@ -5,8 +5,6 @@ import signup from "../signup/thunks/signup"; import login from "../login/thunks/login"; import update from "./thunks/update"; import { toast } from "sonner"; -import getCanisters from "./thunks/getCanisters"; -import { createCanister } from "./thunks/createCanister"; export const buildAuthExtraReducers = (builder: ActionReducerMapBuilder) => { builder @@ -60,62 +58,6 @@ export const buildAuthExtraReducers = (builder: ActionReducerMapBuilder { - state.canister = undefined; - state.canisters = {}; - state.canisterLoading = true; - }) - .addCase(getCanisters.fulfilled, (state, action) => { - state.canisters = action.payload; - if(state.user && state.user.principal in action.payload){ - state.canister = action.payload[state.user.principal]; - } - state.canisterLoading = false; - state.canisterError = null; - }) - .addCase(getCanisters.rejected, (state, action) => { - state.canister = undefined; - state.canisters = {}; - state.canisterLoading = false; - state.canisterError = action.payload as string; - }) - - // createCanister slice - // createCanister.ts - .addCase(createCanister.pending, (state) => { - state.canister = undefined; - if (state.user) { - const {[state.user.principal]: _, ...remainingCanisters} = state.canisters; - state.canisters = remainingCanisters; - } - state.canisterError = null; - state.canisterLoading = true; - }) - .addCase(createCanister.fulfilled, (state, action) => { - // Set the user's canister key when fulfilled - if (state.user) { - state.canister = action.payload; - state.canisters = { - ...state.canisters, - [state.user.principal]: action.payload - } - } - state.canisterError = null; - state.canisterLoading = false; - }) - .addCase(createCanister.rejected, (state, action) => { - state.canister = undefined; - - if (state.user) { - const {[state.user.principal]: _, ...remainingCanisters} = state.canisters; - state.canisters = remainingCanisters; - } - state.canisterError = action.payload as string; - state.canisterLoading = false; - }) - // signup slice // signup.ts .addCase(signup.fulfilled, (state, action:PayloadAction) => { diff --git a/src/alex_frontend/core/features/auth/authSlice.ts b/src/alex_frontend/core/features/auth/authSlice.ts index 1abd8c473..6f9b4b47b 100644 --- a/src/alex_frontend/core/features/auth/authSlice.ts +++ b/src/alex_frontend/core/features/auth/authSlice.ts @@ -14,33 +14,23 @@ export interface SerializedUser { // Define the interface for our auth state export interface AuthState { user: SerializedUser | null, - canister: string | undefined, - canisters: Record, loading: boolean; error: string | null; librarianLoading: boolean librarianError: string | null; - - canisterLoading: boolean - canisterError: string | null; } // Define the initial state using the AuthState interface const initialState: AuthState = { user: null, - canister: undefined, - canisters: {}, loading: false, error: null, librarianLoading: false, librarianError: null, - - canisterLoading: false, - canisterError: null, }; diff --git a/src/alex_frontend/core/features/auth/thunks/createCanister.ts b/src/alex_frontend/core/features/auth/thunks/createCanister.ts deleted file mode 100644 index fa3080186..000000000 --- a/src/alex_frontend/core/features/auth/thunks/createCanister.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ActorSubclass } from "@dfinity/agent"; -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { _SERVICE as ASSET_MANAGER_SERVICE } from "../../../../../../src/declarations/asset_manager/asset_manager.did"; -import { _SERVICE as LBRY_SERVICE} from "../../../../../../src/declarations/LBRY/LBRY.did"; -import { RootState } from "@/store"; -import { Principal } from "@dfinity/principal"; - -export const createCanister = createAsyncThunk< - string, // Canister Id - { - assetManagerActor: ActorSubclass; - lbryActor: ActorSubclass; - }, //Argument that we pass to initialize - { rejectValue: string, state: RootState } ->( - "auth/createCanister", - async ({ assetManagerActor, lbryActor }, { rejectWithValue, getState }) => { - try { - const {user} = getState().auth; - if(!user?.principal) throw new Error('Unauthenticated users not allowed.') - - const assetManagerCanisterId = process.env.CANISTER_ID_ASSET_MANAGER!; - let amountFormatApprove: bigint = BigInt( - Number((Number(10) + 0.04) * 10 ** 8).toFixed(0) - ); - const checkApproval = await lbryActor.icrc2_allowance({ - account: { - owner: Principal.fromText(user.principal), - subaccount: [], - }, - spender: { - owner: Principal.fromText(assetManagerCanisterId), - subaccount: [], - }, - }); - - if (checkApproval.allowance < amountFormatApprove) { - const resultIcpApprove = await lbryActor.icrc2_approve({ - spender: { - owner: Principal.fromText(assetManagerCanisterId), - subaccount: [], - }, - amount: amountFormatApprove, - fee: [], - memo: [], - from_subaccount: [], - created_at_time: [], - expected_allowance: [], - expires_at: [], - }); - if ("Err" in resultIcpApprove) { - const error = resultIcpApprove.Err; - let errorMessage = "Unknown error"; // Default error message - if ("InsufficientFunds" in error) { - errorMessage = "Insufficient balance to process creation. Please swap/deposit some LBRY tokens"; - } - if ("TemporarilyUnavailable" in error) { - errorMessage = "Service is temporarily unavailable"; - } - throw new Error(errorMessage); - } - } - - const result = await assetManagerActor.create_asset_canister(); - - if ("Ok" in result) { - return result.Ok.toString(); - } - if ("Err" in result) { - return rejectWithValue(result.Err.toString()); - } - - return rejectWithValue("Unexpected response format"); - } catch (error) { - console.error("Error creating asset canister:", error); - return rejectWithValue( - error instanceof Error - ? error.message - : "Unknown error occurred" - ); - } - } -); diff --git a/src/alex_frontend/core/features/auth/thunks/getCanisters.ts b/src/alex_frontend/core/features/auth/thunks/getCanisters.ts deleted file mode 100644 index 99cbbeff6..000000000 --- a/src/alex_frontend/core/features/auth/thunks/getCanisters.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ActorSubclass } from "@dfinity/agent"; -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { _SERVICE } from "../../../../../../src/declarations/asset_manager/asset_manager.did"; - -// Define the async thunk -const getCanisters = createAsyncThunk< - Record, // This is the return type of the thunk's payload - { - actor: ActorSubclass<_SERVICE>, - }, //Argument that we pass to initialize - { rejectValue: string } ->( "auth/getCanisters", async ( {actor}, { rejectWithValue }) => { - try { - const result = await actor.get_all_user_asset_canisters(); - - const canisters: Record = {}; - - result.forEach(([owner, canister]) => { - canisters[owner.toString()] = canister.assigned_canister_id.toString(); - }); - - return canisters; - } catch (error) { - console.error("Failed to fetch canisters:", error); - - if (error instanceof Error) { - return rejectWithValue(error.message); - } - } - return rejectWithValue("An unknown error occurred while fetching canisters"); - } -); - -export default getCanisters; \ No newline at end of file diff --git a/src/alex_frontend/core/features/auth/utils/authUtils.ts b/src/alex_frontend/core/features/auth/utils/authUtils.ts index 3525c5a41..0e058fc05 100644 --- a/src/alex_frontend/core/features/auth/utils/authUtils.ts +++ b/src/alex_frontend/core/features/auth/utils/authUtils.ts @@ -58,16 +58,6 @@ import { icp_swap_factory, createActor as createActorIcpSwapFactory, } from "../../../../../icp_swap_factory"; -import { - createActor as createActorAssetCanister, - // asset_canister, // We will not use the potentially undefined global default -} from "../../../../../asset_canister"; - -import { - asset_manager, - createActor as createActorAssetManager, -} from "../../../../../declarations/asset_manager"; - import { perpetua, createActor as createActorPerpetua, @@ -98,8 +88,6 @@ const emporium_canister_id = process.env.CANISTER_ID_EMPORIUM!; const log_canister_id = process.env.CANISTER_ID_LOGS!; const perpetua_canister_id = process.env.CANISTER_ID_PERPETUA!; const icp_swap_factory_canister_id = "ggzvv-5qaaa-aaaag-qck7a-cai"; -const asset_manager_canister_id = process.env.CANISTER_ID_ASSET_MANAGER!; - export const getPrincipal = (client: AuthClient): string => client.getIdentity().getPrincipal().toString(); @@ -222,8 +210,3 @@ export const getIcpSwapFactoryCanister = () => createActorIcpSwapFactory, ); -export const getActorUserAssetCanister = (canisterId: string) => - getActor(canisterId, createActorAssetCanister); - -export const getActorAssetManager = () => - getActor(asset_manager_canister_id, createActorAssetManager); diff --git a/src/alex_frontend/core/features/icp-assets/components/ICPAssetUploader.tsx b/src/alex_frontend/core/features/icp-assets/components/ICPAssetUploader.tsx deleted file mode 100644 index 7e1fd929b..000000000 --- a/src/alex_frontend/core/features/icp-assets/components/ICPAssetUploader.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useState } from "react"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { useAssetManager } from "@/hooks/useAssetManager"; -import upload from "../thunks/upload"; -import { Input } from "@/lib/components/input"; -import { Button } from "@/lib/components/button"; -import { useIdentity } from "@/lib/ic-use-identity"; - -const ICPAssetUploader: React.FC = () => { - const dispatch = useAppDispatch(); - const { canister } = useAppSelector((state) => state.auth); - const {identity} = useIdentity(); - const [file, setFile] = useState(null); - const { uploading, percentage } = useAppSelector((state) => state.icpAssets); - - const handleFileChange = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - setFile(e.target.files[0]); - } - }; - - const assetManager = useAssetManager({ - canisterId: canister ?? undefined, - identity - }); - - const handleUpload = async () => { - if (!file || !assetManager) return; - - try { - await dispatch(upload({ file, assetManager })).unwrap(); - } catch (error) { - console.log('upload failed', error) - } finally{ - setFile(null) - } - }; - - return ( -
    -

    Upload New Asset

    -
    - - -
    - - {uploading && ( -
    -

    - Uploading: {file?.name} ({Math.round(percentage)}%) -

    -
    - {percentage > 0 &&
    } -
    -
    - )} -
    - ); -}; - -export default ICPAssetUploader; \ No newline at end of file diff --git a/src/alex_frontend/core/features/icp-assets/components/ICPAssets.tsx b/src/alex_frontend/core/features/icp-assets/components/ICPAssets.tsx deleted file mode 100644 index dc3a59857..000000000 --- a/src/alex_frontend/core/features/icp-assets/components/ICPAssets.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useEffect } from "react"; -import Item from "./Item"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { useIdentity } from "@/lib/ic-use-identity"; -import { useAssetManager } from "@/hooks/useAssetManager"; -import fetch from "@/features/icp-assets/thunks/fetch"; -import { useAppDispatch } from "@/store/hooks/useAppDispatch"; - -const ICPAssets: React.FC = () => { - const dispatch = useAppDispatch(); - const { canister } = useAppSelector((state) => state.auth); - const { identity } = useIdentity(); - const { assets, loading } = useAppSelector((state) => state.icpAssets); - - const assetManager = useAssetManager({ - canisterId: canister ?? undefined, - identity, - }); - - useEffect(() => { - if (!assetManager) return; - dispatch(fetch({ assetManager })); - }, [assetManager]); - - if (loading) { - return ( -
    -
    -
    -

    Loading assets...

    -
    -
    - ); - } - - if (!assets || assets.length <= 0) { - return ( -
    -

    No assets found in your canister.

    -
    - ); - } - - return ( -
    -

    Assets on your canister

    -
    - {assets.map((asset) => ( - - ))} -
    -
    - ); -}; - -export default ICPAssets; diff --git a/src/alex_frontend/core/features/icp-assets/components/Item.tsx b/src/alex_frontend/core/features/icp-assets/components/Item.tsx deleted file mode 100644 index 642cf9f82..000000000 --- a/src/alex_frontend/core/features/icp-assets/components/Item.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from "react"; -import { useAppSelector } from "@/store/hooks/useAppSelector"; -import { FileText } from "lucide-react"; -import { getFileTypeInfo } from "@/features/pinax/constants"; -import { IcpAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; -import remove from "../thunks/remove"; -import { useAppDispatch } from "@/store/hooks/useAppDispatch"; -import { Button } from "@/lib/components/button"; - -const isLocal = process.env.DFX_NETWORK == "local"; - -interface ItemProps { - asset: IcpAssetItem; - assetManager: AssetManager | null; -} - -const Item: React.FC = ({ asset, assetManager }) => { - const {canister} = useAppSelector(state=>state.auth); - const dispatch = useAppDispatch(); - const {deleting} = useAppSelector(state=>state.icpAssets); - - const handleDelete = async () => { - if (!assetManager) return; - - dispatch(remove({ asset, assetManager })); - }; - - const fileTypeInfo = getFileTypeInfo(asset.content_type); - const isImage = fileTypeInfo?.label === 'Images'; - const isVideo = fileTypeInfo?.label === 'Media' && asset.content_type.startsWith('video'); - const isAudio = fileTypeInfo?.label === 'Media' && asset.content_type.startsWith('audio'); - const isDocument = fileTypeInfo?.label === 'Documents' || fileTypeInfo?.label === 'E-books'; - - // Generate canister asset URL - const getCanisterAssetUrl = () => { - if (!canister) return ""; - const baseUrl = isLocal ? `http://${canister}.localhost:4943` : `https://${canister}.raw.icp0.io`; - return baseUrl + asset.key; - }; - - return ( -
    -
    - {isImage ? ( - {asset.key} - ) : isVideo ? ( - - ) : isAudio ? ( -
    - -
    - ) : isDocument ? ( -
    - {fileTypeInfo?.icon} -

    {fileTypeInfo?.label}

    -
    - ) : ( -
    - -

    File

    -
    - )} -
    - -
    -

    {asset.key}

    - -
    -
    - Type: - - {asset.content_type} - -
    -
    - -
    - - View File - - -
    -
    -
    - ); -}; - -export default Item; \ No newline at end of file diff --git a/src/alex_frontend/core/features/icp-assets/icpAssetsSlice.ts b/src/alex_frontend/core/features/icp-assets/icpAssetsSlice.ts deleted file mode 100644 index 2fcd423a2..000000000 --- a/src/alex_frontend/core/features/icp-assets/icpAssetsSlice.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { IcpAssetItem, IcpAssetsState } from "./types"; -import remove from "./thunks/remove"; -import fetch from "./thunks/fetch"; -import upload from "./thunks/upload"; -import { deleteAssetFromCanister } from "../arweave-assets/thunks/deleteAssetFromCanister"; -import { pullAssetToCanister } from "../arweave-assets/thunks/pullAssetToCanister"; - -const initialState: IcpAssetsState = { - assets: [], - - uploading: false, - percentage: 0, - uploadError: null, - - deleting: null, - deleteError: null, - - loading: false, - error: null, -}; - -const icpAssetsSlice = createSlice({ - name: "icpAssets", - initialState, - reducers: { - setProgress: (state, action: PayloadAction) => { - state.percentage = action.payload; - }, - addAsset: (state, action: PayloadAction) => { - state.assets = [...state.assets, action.payload]; - }, - setAssets: (state, action: PayloadAction) => { - state.assets = action.payload; - }, - }, - extraReducers: (builder) => { - builder - .addCase(fetch.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase(fetch.fulfilled, (state, action) => { - state.loading = false; - state.assets = action.payload; - }) - .addCase(fetch.rejected, (state, action) => { - state.loading = false; - state.error = action.payload || "Failed to fetch assets"; - }) - - .addCase(remove.pending, (state, action) => { - state.deleting = action.meta.arg.asset; - state.deleteError = null; - }) - .addCase(remove.fulfilled, (state, action) => { - state.deleting = null; - state.deleteError = null; - state.assets = state.assets.filter(asset => asset.key !== action.meta.arg.asset.key); - }) - .addCase(remove.rejected, (state, action) => { - state.deleting = null; - state.deleteError = action.payload as string; - }) - - .addCase(upload.pending, (state, action) => { - state.percentage = 0; - state.uploading = true; - state.uploadError = null; - }) - .addCase(upload.fulfilled, (state, action) => { - state.uploading = false; - state.uploadError = null; - state.assets = [...state.assets, action.payload]; - }) - .addCase(upload.rejected, (state, action) => { - state.uploading = false; - state.uploadError = action.payload as string; - }) - - .addCase(deleteAssetFromCanister.fulfilled, (state, action) => { - state.deleting = null; - state.deleteError = null; - state.assets = state.assets.filter(asset => - asset.key !== `/arweave/${action.meta.arg.asset.id}` - ); - }) - - .addCase(pullAssetToCanister.fulfilled, (state, action) => { - state.assets = [...state.assets, { - key: `/arweave/${action.meta.arg.asset.id}`, - content_type: action.meta.arg.asset.contentType, - encodings: [], - } as IcpAssetItem]; - }) - }, -}); - -export const { setAssets, setProgress, addAsset } = icpAssetsSlice.actions; -export default icpAssetsSlice.reducer; diff --git a/src/alex_frontend/core/features/icp-assets/thunks/fetch.ts b/src/alex_frontend/core/features/icp-assets/thunks/fetch.ts deleted file mode 100644 index d9f269ade..000000000 --- a/src/alex_frontend/core/features/icp-assets/thunks/fetch.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { IcpAssetItem } from "../types"; -import { AssetManager } from "@dfinity/assets"; - -const fetch = createAsyncThunk< - IcpAssetItem[], - {assetManager: AssetManager}, - { rejectValue: string } ->("icpAssets/fetch", async ({assetManager}, { rejectWithValue }) => { - try { - // Fetch assets directly using the assetManager from the hook - const assets = await assetManager.list(); - - // get recently modified assets - const filteredAssets = assets - // Filter assets starting with '/uploads/' if needed (or remove if not needed) - // .filter(asset => asset.key.startsWith('/uploads/')) - .sort((a, b) => Number(b.encodings[0].modified) - Number(a.encodings[0].modified)) - .map(asset => ({ - ...asset, - encodings: asset.encodings.map(encoding => ({ - content_encoding: encoding.content_encoding, - modified: Number(encoding.modified), - length: Number(encoding.length) - })) - })); - - return filteredAssets; - } catch (error) { - console.error("Error fetching user assets:", error); - return rejectWithValue( - error instanceof Error ? error.message : "Unknown error occurred" - ); - } -}); - -export default fetch; \ No newline at end of file diff --git a/src/alex_frontend/core/features/icp-assets/thunks/remove.ts b/src/alex_frontend/core/features/icp-assets/thunks/remove.ts deleted file mode 100644 index d0a6e01d4..000000000 --- a/src/alex_frontend/core/features/icp-assets/thunks/remove.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { AssetManager } from "@dfinity/assets"; -import { IcpAssetItem } from "../types"; - -const remove = createAsyncThunk< - void, - { - asset: IcpAssetItem, - assetManager: AssetManager, - }, - { rejectValue: string } ->("icpAssets/remove", async ({asset, assetManager}, { rejectWithValue }) => { - try { - await assetManager.delete(asset.key); - } catch (error) { - console.error("Error deleting file from canister:", error); - return rejectWithValue(error instanceof Error ? error.message : "Unknown error occurred"); - } -}); - -export default remove; \ No newline at end of file diff --git a/src/alex_frontend/core/features/icp-assets/thunks/upload.ts b/src/alex_frontend/core/features/icp-assets/thunks/upload.ts deleted file mode 100644 index b892fc508..000000000 --- a/src/alex_frontend/core/features/icp-assets/thunks/upload.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { AppDispatch } from "@/store"; -import { setProgress } from "../icpAssetsSlice"; -import { AssetManager } from "@dfinity/assets"; -import { IcpAssetItem } from "../types"; - -const upload = createAsyncThunk< - IcpAssetItem, // This is the return type of the thunk's payload - { - file: File; - assetManager: AssetManager; - }, //Argument that we pass to initialize - { rejectValue: string; dispatch: AppDispatch } ->( - "icpAssets/upload", - async ( - { file, assetManager }, - { rejectWithValue, dispatch } - ) => { - try { - - // Use AssetManager's batch upload which handles chunking - const batch = assetManager.batch(); - const key = await batch.store(file, { path: '/uploads' }); - - // Commit the batch with progress tracking - await batch.commit({ - onProgress: ({current, total}: {current: number, total: number}) => { - const progressPercent = (current / total) * 100; - dispatch(setProgress(progressPercent)); - } - }); - - return { - key, - content_type: file.type, - encodings: [] - } - } catch (error) { - console.error("Failed to Upload File:", error); - - if (error instanceof Error) { - return rejectWithValue(error.message); - } - } - return rejectWithValue( - "An unknown error occurred while uploading file" - ); - } -); - - -export default upload; \ No newline at end of file diff --git a/src/alex_frontend/core/features/icp-assets/types.ts b/src/alex_frontend/core/features/icp-assets/types.ts deleted file mode 100644 index 0dd98b8f7..000000000 --- a/src/alex_frontend/core/features/icp-assets/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface IcpAssetItem { - key: string; - encodings: Array<{ - modified: number; - length: number; - content_encoding: string; - }>; - content_type: string; -} - -export interface IcpAssetsState { - assets: IcpAssetItem[]; - - uploading: boolean; - percentage: number; - uploadError: string | null; - - deleting: IcpAssetItem | null; - deleteError: string | null; - - loading: boolean; - error: string | null; -} diff --git a/src/alex_frontend/core/features/login/index.tsx b/src/alex_frontend/core/features/login/index.tsx index 32615668f..1975542c3 100644 --- a/src/alex_frontend/core/features/login/index.tsx +++ b/src/alex_frontend/core/features/login/index.tsx @@ -109,7 +109,7 @@ const Login:React.FC = ({ fullpage = false }) => { + + + + + + Add Item + + Add content to this shelf. + + +
    + {error && ( +
    + + {error} +
    + )} + + + + Markdown + NFT + Shelf + + + +