diff --git a/src/features/main/components/Main.tsx b/src/features/main/components/Main.tsx index f135125..1745017 100644 --- a/src/features/main/components/Main.tsx +++ b/src/features/main/components/Main.tsx @@ -3,7 +3,7 @@ import { createAoContractClientForProcess } from "@/features/ao/lib/aoContractCl import { AoWallet } from "@/features/ao/lib/aoWallet"; import { truncateAddress } from "@/features/arweave/lib/utils"; import { createChatClientForProcess } from "@/features/chat/contract/chatClient"; -import { ProfileInfo } from "@/features/profile/contract/model"; +import { ProfileAssets, ProfileInfo } from "@/features/profile/contract/model"; import { createProfileRegistryClientForProcess } from "@/features/profile/contract/profileRegistryClient"; import { Renderer } from "@/features/render/components/Renderer"; import { createRealityClientForProcess } from "@/features/reality/contract/realityClient"; @@ -11,6 +11,7 @@ import { mainMachine } from "../machines/mainMachine"; import { useMachine } from "@xstate/react"; import ProfileButton from "@/features/profile/components/ProfileButton"; import { createTrackingClientForProcess } from "@/features/tracking/contract/trackingClient"; +import { createProfileClientForProcess } from "@/features/profile/contract/profileClient"; const profileRegistryProcessId = import.meta.env .VITE_PROFILE_PROCESS_ID as string; @@ -29,12 +30,23 @@ export default function Main({ wallet, disconnect, worldId }: MainProps) { import.meta.env.VITE_TRACKING_TEST_PROCESS_ID, ); + const aoContractClientForProcess = createAoContractClientForProcess(wallet); + + const profileClientForProcess = createProfileClientForProcess(wallet); + + const realityClientForProcess = createRealityClientForProcess(wallet); + + const realityClientBaseWorldId = realityClientForProcess(worldId || ""); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [current, send] = useMachine(mainMachine, { input: { initialContext: { address: wallet.address, profileRegistryClient, + profileClientForProcess, + aoContractClientForProcess, + realityClientBaseWorldId }, }, }); @@ -42,13 +54,14 @@ export default function Main({ wallet, disconnect, worldId }: MainProps) { const renderer = (profile?: { profileId: string; profileInfo: ProfileInfo; + assets: ProfileAssets; }) => (

- + @@ -79,6 +92,7 @@ export default function Main({ wallet, disconnect, worldId }: MainProps) { renderer({ profileId: current.context.profileId!, profileInfo: current.context.profileInfo!, + assets: current.context.assets!, }) ) : ( renderer() diff --git a/src/features/main/machines/mainMachine.ts b/src/features/main/machines/mainMachine.ts index d9f81e3..38f9f57 100644 --- a/src/features/main/machines/mainMachine.ts +++ b/src/features/main/machines/mainMachine.ts @@ -1,10 +1,16 @@ -import { ProfileInfo } from "@/features/profile/contract/model"; +import { AoContractClientForProcess, ReadArgs } from "@/features/ao/lib/aoContractClient"; +import { Asset, DetailedAsset, ProfileAssets, ProfileInfo } from "@/features/profile/contract/model"; +import { ProfileClientForProcess } from "@/features/profile/contract/profileClient"; import { ProfileRegistryClient } from "@/features/profile/contract/profileRegistryClient"; +import { RealityClient } from "@/features/reality/contract/realityClient"; import { fromPromise, setup, assign } from "xstate"; type InitialContext = { address: string; profileRegistryClient: ProfileRegistryClient; + profileClientForProcess: ProfileClientForProcess; + aoContractClientForProcess: AoContractClientForProcess; + realityClientBaseWorldId: RealityClient; }; export const mainMachine = setup({ @@ -15,6 +21,7 @@ export const mainMachine = setup({ context: {} as InitialContext & { profileId?: string; profileInfo?: ProfileInfo; + assets?: ProfileAssets; }, }, guards: { @@ -23,8 +30,9 @@ export const mainMachine = setup({ params: { profileId?: string; profileInfo?: ProfileInfo; + assets?: ProfileAssets; }, - ) => params.profileId !== undefined && params.profileInfo !== undefined, + ) => params.profileId !== undefined && params.profileInfo !== undefined && params.assets !== undefined, }, actions: { assignProfileIdAndInfo: assign( @@ -33,10 +41,12 @@ export const mainMachine = setup({ params: { profileId: string; profileInfo: ProfileInfo; + assets: ProfileAssets; }, ) => ({ profileId: params.profileId, profileInfo: params.profileInfo, + assets: params.assets }), ), }, @@ -48,13 +58,17 @@ export const mainMachine = setup({ input: { address: string; profileRegistryClient: ProfileRegistryClient; + bazarProfileClient: ProfileClientForProcess; + aoContractClientForProcess: AoContractClientForProcess; + realityClient: RealityClient; }; }) => { - const { address, profileRegistryClient } = input; + const { address, profileRegistryClient, bazarProfileClient, aoContractClientForProcess, realityClient } = input; const noProfile = { profileId: undefined, profileInfo: undefined, + assets: undefined, }; const profiles = @@ -68,9 +82,71 @@ export const mainMachine = setup({ ]); if (profileInfos.length === 0) return noProfile; + let profileClient = bazarProfileClient(primaryProfileId) + + let responseData = await profileClient.grabProfileAssets() + // Assuming responseData.Data is a string, we first need to parse it + const dataObject = JSON.parse(responseData.Data); + + // Now we can extract the Assets array from the parsed JSON + const assetsData = dataObject.Assets; + + // Info Arguments + const args: ReadArgs = { + tags: [{ name: "Action", value: "Info" }], + }; + + // Fetch parameters + let parameters = await realityClient.readParameters(); + + console.log(parameters); + + // Extract the whitelist (or null if it doesn't exist) + const whitelist = parameters["asset-whitelist"]?.Whitelist ?? null; + + // Filter and process assets (if whitelist exists, filter by it; otherwise, process all assets) + let detailedAssets: DetailedAsset[] = await Promise.all( + assetsData + .filter((asset: any) => { + // If whitelist is null, don't filter (show everything); otherwise, filter by whitelist + return whitelist === null || whitelist.includes(asset.Id); + }) + .map(async (asset: any) => { + let assetFormal: Asset = Asset.parse(asset); + + let assetClient = aoContractClientForProcess(asset.Id); + let message = await assetClient.dryrunReadReplyOne(args); + + let logoTag = message.Tags.find((tag) => tag.name === "Logo"); + let denominationTag = message.Tags.find((tag) => tag.name === "Denomination"); + + if (denominationTag !== undefined) { + let denominator = 10 ** Number(denominationTag.value); + let quantityValue = Number(assetFormal.Quantity); + assetFormal.Quantity = (quantityValue / denominator).toString(); + } + + let detailedAsset; + + if (logoTag === undefined) { + detailedAsset = DetailedAsset.parse({ asset: assetFormal, icon: assetFormal.Id }); + } else { + detailedAsset = DetailedAsset.parse({ asset: assetFormal, icon: logoTag.value }); + } + + return detailedAsset; + }) + ); + + // Parsing and validating the data with the ProfileAssets schema + const profileAssets = ProfileAssets.parse(detailedAssets); + + console.log("Validated Profile Assets:", profileAssets); + return { profileId: primaryProfileId, profileInfo: profileInfos[0], + assets: profileAssets, }; }, ), @@ -96,6 +172,9 @@ export const mainMachine = setup({ input: ({ context }) => ({ address: context.address, profileRegistryClient: context.profileRegistryClient, + bazarProfileClient: context.profileClientForProcess, + aoContractClientForProcess: context.aoContractClientForProcess, + realityClient: context.realityClientBaseWorldId }), onDone: [ @@ -110,6 +189,7 @@ export const mainMachine = setup({ params: ({ event }) => ({ profileId: event.output.profileId!, profileInfo: event.output.profileInfo!, + assets: event.output.assets!, }), }, }, diff --git a/src/features/profile/components/AssetCard.tsx b/src/features/profile/components/AssetCard.tsx new file mode 100644 index 0000000..61c06dd --- /dev/null +++ b/src/features/profile/components/AssetCard.tsx @@ -0,0 +1,23 @@ +import { fetchUrl } from '@/features/arweave/lib/arweave'; +import { DetailedAsset } from '../contract/model'; // Adjust the path as needed + +interface AssetCardProps { + asset?: DetailedAsset; // Make asset optional +} + +export default function AssetCard({ asset }: AssetCardProps) { + if (!asset) return null; // Return nothing if no asset + + return ( +
+ {asset.asset.Id} + {asset.asset.Quantity && parseInt(asset.asset.Quantity) > 1 && ( +
+ {asset.asset.Quantity} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/features/profile/components/ProfileAssetsDisplay.tsx b/src/features/profile/components/ProfileAssetsDisplay.tsx new file mode 100644 index 0000000..954b9c5 --- /dev/null +++ b/src/features/profile/components/ProfileAssetsDisplay.tsx @@ -0,0 +1,23 @@ +import { DetailedAsset } from '../contract/model'; // Adjust the path as needed +import AssetCard from './AssetCard'; + +interface ProfileAssetsDisplayProps { + assets: DetailedAsset[]; +} + +export default function ProfileAssetsDisplay({ assets }: ProfileAssetsDisplayProps) { + const totalCells = Math.max(4, Math.ceil(assets.length / 4) * 4); + + return ( +
+ {[...Array(totalCells)].map((_, index) => ( +
+ {assets[index] && } +
+ ))} +
+ ); +} diff --git a/src/features/profile/components/ProfileButton.tsx b/src/features/profile/components/ProfileButton.tsx index cf43e7d..3493ccd 100644 --- a/src/features/profile/components/ProfileButton.tsx +++ b/src/features/profile/components/ProfileButton.tsx @@ -3,7 +3,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { ProfileInfo } from "../contract/model"; +import { ProfileAssets, ProfileInfo } from "../contract/model"; import ProfileDetailsDropdown from "./ProfileDetailsDropdown"; import { Card, @@ -16,9 +16,10 @@ import ProfileImage from "./ProfileImage"; interface ProfileButtonProps { profileInfo?: ProfileInfo; + assets?: ProfileAssets; } -export default function ProfileButton({ profileInfo }: ProfileButtonProps) { +export default function ProfileButton({ profileInfo, assets }: ProfileButtonProps) { return ( @@ -26,7 +27,7 @@ export default function ProfileButton({ profileInfo }: ProfileButtonProps) { {profileInfo ? ( - + ) : ( diff --git a/src/features/profile/components/ProfileDetailsDropdown.tsx b/src/features/profile/components/ProfileDetailsDropdown.tsx index 88fd013..bcc553a 100644 --- a/src/features/profile/components/ProfileDetailsDropdown.tsx +++ b/src/features/profile/components/ProfileDetailsDropdown.tsx @@ -1,13 +1,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { ProfileInfo } from "../contract/model"; +import { ProfileAssets, ProfileInfo } from "../contract/model"; import ProfileImage from "./ProfileImage"; +import ProfileAssetsDisplay from './ProfileAssetsDisplay'; // Import your new component interface ProfileDetailsDropdownProps { profileInfo: ProfileInfo; + profileAssets?: ProfileAssets; } export default function ProfileDetailsDropdown({ profileInfo, + profileAssets }: ProfileDetailsDropdownProps) { const hasDescription = profileInfo.Description && profileInfo.Description != ""; @@ -32,8 +35,12 @@ export default function ProfileDetailsDropdown({ ) : (

No bio

)} +
+

Assets

+ +

- Edit your profile on{" "} + Edit your profile and view all your assets not just World specific ones on:{" "} ; + +// Define the Asset schema +export const Asset = z.object({ + Quantity: z.string(), + Id: z.string(), +}); +export type Asset = z.infer; + +// Define the DetailedAsset schema with an optional icon +export const DetailedAsset = z.object({ + asset: Asset, + icon: z.string(), // Making icon optional +}); +export type DetailedAsset = z.infer; + +// Define the ProfileAssets schema as an array of DetailedAssets +export const ProfileAssets = z.array(DetailedAsset); +export type ProfileAssets = z.infer; \ No newline at end of file diff --git a/src/features/profile/contract/profileClient.ts b/src/features/profile/contract/profileClient.ts index df6b8f6..8a2b6b0 100644 --- a/src/features/profile/contract/profileClient.ts +++ b/src/features/profile/contract/profileClient.ts @@ -7,12 +7,13 @@ import { connect } from "@permaweb/aoconnect"; import { profileAOS } from "./config"; import { fetchUrl } from "@/features/arweave/lib/arweave"; import { ProfileInfoCreate } from "./model"; -import { MessageResult } from "@/features/ao/lib/aoClient"; +import { Message, MessageResult } from "@/features/ao/lib/aoClient"; export type ProfileClient = { aoContractClient: AoContractClient; // Reads + grabProfileAssets(): Promise; // Writes initializeProcess(): Promise; @@ -26,6 +27,15 @@ export const createProfileClient = ( ): ProfileClient => ({ aoContractClient: aoContractClient, + // Reads + grabProfileAssets: async () => { + const message = await aoContractClient.dryrunReadReplyOne({ + tags: [{ name: "Action", value: "Info" }], + }); + console.log(message, ' ', message.Data) + return message; + }, + // Writes initializeProcess: async () => { const profileSrc = await fetch(fetchUrl(profileAOS.profileSrc)).then( @@ -48,8 +58,10 @@ export const createProfileClient = ( }), }); +export type ProfileClientForProcess = (processId: string) => ProfileClient; + export const createProfileClientForProcess = - (wallet: AoWallet) => (processId: string) => { + (wallet: AoWallet): ProfileClientForProcess => (processId: string) => { const aoContractClient = createAoContractClient( processId, connect(), diff --git a/src/features/reality/contract/model.ts b/src/features/reality/contract/model.ts index 43c5404..49b3b98 100644 --- a/src/features/reality/contract/model.ts +++ b/src/features/reality/contract/model.ts @@ -25,12 +25,21 @@ export const RealityToken = z.object({ }); export type RealityToken = z.infer; +// Defining a constant for whitelist schema +export const RealityWhitelist = z.object({ + Whitelist: z.array(ArweaveAddress) // Array of Arweave addresses +}); +export type RealityWhitelist = z.infer; + +// RealityParameters defines the full schema for parameters including token, bounds, 2D-Tile, Audio, and Whitelist export const RealityParameters = z.object({ - Token: z.optional(RealityToken), - Bounds: z.optional(RealityParameterBounds), - "2D-Tile-0": z.optional(_2dTileParams), - "Audio-0": z.optional(AudioParams), + Token: z.optional(RealityToken), // Optional token object + Bounds: z.optional(RealityParameterBounds), // Optional bounds object + "2D-Tile-0": z.optional(_2dTileParams), // Optional 2D tile parameters + "Audio-0": z.optional(AudioParams), // Optional audio parameters + "asset-whitelist": z.optional(RealityWhitelist), // Optional whitelist object }); + export type RealityParameters = z.infer; export const RealityEntityType = z.enum(["Unknown", "Avatar", "Hidden"]);