UNPKG

@starknet-react/core

Version:

Starknet React is a collection of React hooks for Starknet.

514 lines (477 loc) 13.6 kB
import type { Address } from "@starknet-react/chains"; import { useMemo } from "react"; import { CairoCustomEnum, cairo, hash, shortString, starknetId, } from "starknet"; import { type UseQueryProps, type UseQueryResult, useQuery } from "../query"; import { type StarknetTypedContract, useContract } from "./use-contract"; import { useNetwork } from "./use-network"; /** Arguments for `useStarkProfile` hook. */ export type StarkProfileArgs = UseQueryProps< GetStarkprofileResponse, Error, GetStarkprofileResponse, ReturnType<typeof queryKey> > & { /** Account address. */ address?: Address; /** Get Starknet ID default pfp url if no profile picture is set */ useDefaultPfp?: boolean; /** Naming contract to use. */ namingContract?: Address; /** Identity contract to use. */ identityContract?: Address; }; /** Value returned by `useStarkProfile` hook. */ export type GetStarkprofileResponse = { name?: string; /** Metadata url of the NFT set as profile picture. */ profile?: string; /** Profile picture url. */ profilePicture?: string; twitter?: string; github?: string; discord?: string; proofOfPersonhood: boolean; }; export type UseStarkProfileResult = UseQueryResult< GetStarkprofileResponse, Error >; type Contract = StarknetTypedContract<typeof multicallABI>; /** * Hook for fetching Stark profile for address. * * @remarks * * This hook fetches the stark name of the specified address, profile picture url, * social networks ids, and proof of personhood a user has set on its starknetid. * It defaults to the starknet.id naming and identity contracts but different contracts can be * targetted by specifying their contract addresses * */ export function useStarkProfile({ address, useDefaultPfp = true, namingContract, identityContract, enabled: enabled_ = true, ...props }: StarkProfileArgs): UseStarkProfileResult { const { chain } = useNetwork(); if (!StarknetIdcontracts[chain.network]) throw new Error("Network not supported"); const { contract: multicallContract } = useContract({ abi: multicallABI, address: StarknetIdcontracts[chain.network]["multicall"], }); const enabled = useMemo( () => Boolean(enabled_ && address), [enabled_, address], ); const { refetchInterval, ...rest } = props; return useQuery({ queryKey: queryKey({ address, namingContract, identityContract, network: chain.network, useDefaultPfp, }), queryFn: queryFn({ address, useDefaultPfp, namingContract, network: chain.network, identityContract, multicallContract, }), enabled, refetchInterval, ...rest, }); } function queryKey({ address, namingContract, identityContract, network, useDefaultPfp, }: { address?: string; namingContract?: string; identityContract?: string; network?: string; useDefaultPfp?: boolean; }) { return [ { entity: "starkprofile", address, namingContract, identityContract, network, useDefaultPfp, }, ] as const; } function queryFn({ address, useDefaultPfp, namingContract, identityContract, network, multicallContract, }: StarkProfileArgs & { multicallContract?: Contract; network?: string; }) { return async () => { if (!address) throw new Error("address is required"); if (!multicallContract) throw new Error("multicallContract is required"); if (!network) throw new Error("network is required"); const contracts = StarknetIdcontracts[network]; const identity = identityContract ?? contracts["identity"]; const naming = namingContract ?? contracts["naming"]; const data = await multicallContract.call("aggregate", [ [ { execution: staticExecution(), to: hardcoded(naming), selector: hardcoded(hash.getSelectorFromName("address_to_domain")), calldata: [hardcoded(address), hardcoded(0)], }, { execution: staticExecution(), to: hardcoded(naming), selector: hardcoded(hash.getSelectorFromName("domain_to_id")), calldata: [arrayReference(0, 0)], }, { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded(hash.getSelectorFromName("get_verifier_data")), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("twitter")), hardcoded(contracts["verifier"] as string), hardcoded("0"), ], }, { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded(hash.getSelectorFromName("get_verifier_data")), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("github")), hardcoded(contracts["verifier"] as string), hardcoded("0"), ], }, { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded(hash.getSelectorFromName("get_verifier_data")), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("discord")), hardcoded(contracts["verifier"] as string), hardcoded("0"), ], }, { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded(hash.getSelectorFromName("get_verifier_data")), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("proof_of_personhood")), hardcoded(contracts["verifier_pop"] as string), hardcoded("0"), ], }, // PFP { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded(hash.getSelectorFromName("get_verifier_data")), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("nft_pp_contract")), hardcoded(contracts["verifier_pfp"] as string), hardcoded("0"), ], }, { execution: staticExecution(), to: hardcoded(identity), selector: hardcoded( hash.getSelectorFromName("get_extended_verifier_data"), ), calldata: [ reference(1, 0), hardcoded(shortString.encodeShortString("nft_pp_id")), hardcoded("2"), hardcoded(contracts["verifier_pfp"] as string), hardcoded("0"), ], }, { execution: notEqual(6, 0, 0), to: reference(6, 0), selector: hardcoded(hash.getSelectorFromName("tokenURI")), calldata: [reference(7, 1), reference(7, 2)], }, ], ]); if (Array.isArray(data)) { const name = data[0][0] !== BigInt(0) ? // biome-ignore lint/correctness/useHookAtTopLevel: <> starknetId.useDecoded(data[0].slice(1).map((val) => BigInt(val))) : undefined; const twitter = data[2][0] !== BigInt(0) ? data[2][0].toString() : undefined; const github = data[3][0] !== BigInt(0) ? data[3][0].toString() : undefined; const discord = data[4][0] !== BigInt(0) ? data[4][0].toString() : undefined; const proofOfPersonhood = data[5][0] === BigInt(1); const profile = data.length === 9 ? data[8] .slice(1) .map((val) => shortString.decodeShortString(val.toString())) .join("") : undefined; // extract nft_image from profile data const profilePicture = profile ? profile.includes("base64") ? parseBase64Image(profile) : await fetchImageUrl(profile) : useDefaultPfp ? `https://starknet.id/api/identicons/${data[1][0].toString()}` : undefined; const res: GetStarkprofileResponse = { name, twitter, github, discord, proofOfPersonhood, profilePicture, profile, }; return res; } throw new Error("Error while fetching data"); }; } const hardcoded = (arg: string | number) => { return new CairoCustomEnum({ Hardcoded: arg, }); }; const reference = (call: number, pos: number) => { return new CairoCustomEnum({ Reference: cairo.tuple(call, pos), }); }; const arrayReference = (call: number, pos: number) => { return new CairoCustomEnum({ ArrayReference: cairo.tuple(call, pos), }); }; const staticExecution = () => { return new CairoCustomEnum({ Static: {}, }); }; const notEqual = (call: number, pos: number, value: number) => { return new CairoCustomEnum({ IfNotEqual: cairo.tuple(call, pos, value), }); }; const parseBase64Image = (metadata: string): string => { return JSON.parse(atob(metadata.split(",")[1].slice(0, -1))).image; }; const parseImageUrl = (url: string): string => { return url.startsWith("ipfs://") ? url.replace("ipfs://", "https://ipfs.io/ipfs/") : url; }; const fetchImageUrl = async (url: string): Promise<string | undefined> => { try { const response = await fetch(parseImageUrl(url)); if (!response.ok) { throw new Error("Network response was not ok"); } const data = await response.json(); // Check if the "image" key exists and is not null if (data.image) { return parseImageUrl(data.image); } return undefined; } catch (error) { console.error("There was a problem fetching the image URL:", error); return undefined; } }; type StarknetIdContractTypes = { [network: string]: { naming: Address; identity: Address; verifier: Address; verifier_pop: Address; verifier_pfp: Address; multicall: Address; }; }; const StarknetIdcontracts: StarknetIdContractTypes = { sepolia: { naming: "0x154bc2e1af9260b9e66af0e9c46fc757ff893b3ff6a85718a810baf1474", identity: "0x3697660a0981d734780731949ecb2b4a38d6a58fc41629ed611e8defda", verifier: "0x60B94fEDe525f815AE5E8377A463e121C787cCCf3a36358Aa9B18c12c4D566", verifier_pop: "0x15ae88ae054caa74090b89025c1595683f12edf7a4ed2ad0274de3e1d4a", verifier_pfp: "0x9e7bdb8dabd02ea8cfc23b1d1c5278e46490f193f87516ed5ff2dfec02", multicall: "0x07a9013697371ce40d0306b4c810c6a4db9bfda119dd9ae1e8701c8e288d734b", }, mainnet: { naming: "0x6ac597f8116f886fa1c97a23fa4e08299975ecaf6b598873ca6792b9bbfb678", identity: "0x05dbdedc203e92749e2e746e2d40a768d966bd243df04a6b712e222bc040a9af", verifier: "0x07d14dfd8ee95b41fce179170d88ba1f0d5a512e13aeb232f19cfeec0a88f8bf", verifier_pop: "0x0293eb2ba9862f762bd3036586d5755a782bd22e6f5028320f1d0405fd47bff4", verifier_pfp: "0x070aaa20ec4a46da57c932d9fd89ca5e6bb9ca3188d3df361a32306aff7d59c7", multicall: "0x034ffb8f4452df7a613a0210824d6414dbadcddce6c6e19bf4ddc9e22ce5f970", }, } as const; const multicallABI = [ { name: "ComposableMulticallImpl", type: "impl", interface_name: "composable_multicall::IComposableMulticall", }, { name: "composable_multicall::Execution", type: "enum", variants: [ { name: "Static", type: "()", }, { name: "IfEqual", type: "(core::integer::u32, core::integer::u32, core::felt252)", }, { name: "IfNotEqual", type: "(core::integer::u32, core::integer::u32, core::felt252)", }, ], }, { name: "composable_multicall::DynamicFelt", type: "enum", variants: [ { name: "Hardcoded", type: "core::felt252", }, { name: "Reference", type: "(core::integer::u32, core::integer::u32)", }, ], }, { name: "composable_multicall::DynamicCalldata", type: "enum", variants: [ { name: "Hardcoded", type: "core::felt252", }, { name: "Reference", type: "(core::integer::u32, core::integer::u32)", }, { name: "ArrayReference", type: "(core::integer::u32, core::integer::u32)", }, ], }, { name: "composable_multicall::DynamicCall", type: "struct", members: [ { name: "execution", type: "composable_multicall::Execution", }, { name: "to", type: "composable_multicall::DynamicFelt", }, { name: "selector", type: "composable_multicall::DynamicFelt", }, { name: "calldata", type: "core::array::Array::<composable_multicall::DynamicCalldata>", }, ], }, { name: "core::array::Span::<core::felt252>", type: "struct", members: [ { name: "snapshot", type: "@core::array::Array::<core::felt252>", }, ], }, { name: "composable_multicall::IComposableMulticall", type: "interface", items: [ { name: "aggregate", type: "function", inputs: [ { name: "calls", type: "core::array::Array::<composable_multicall::DynamicCall>", }, ], outputs: [ { type: "core::array::Array::<core::array::Span::<core::felt252>>", }, ], state_mutability: "view", }, ], }, { kind: "enum", name: "composable_multicall::contract::ComposableMulticall::Event", type: "event", variants: [], }, ] as const;