0

Abstract App Voting

PreviousNext

A button that allows users to vote for an app on the Abstract portal.

Installation

Ensure you have installed and setup the AGW Provider wrapper component first.

pnpm dlx shadcn@latest add "https://build.abs.xyz/r/abstract-app-voting.json"

Note: Voting is only available on Abstract Mainnet. The component will throw an error if used on other networks.

Usage

Basic Usage

"use client"
 
import { AbstractVotingButton } from "@/components/abstract-voting-button"
 
export default function VotingExample() {
  return (
    <AbstractVotingButton appId="1" />
  )
}

With Event Handlers

"use client"
 
import { AbstractVotingButton } from "@/components/abstract-voting-button"
import { toast } from "sonner"
 
export default function VotingWithToast() {
  return (
    <AbstractVotingButton 
      appId="1"
      onVoteSuccess={() => {
        toast.success("Vote Submitted!", {
          description: "Your vote has been recorded.",
        })
      }}
      onVoteError={(error) => {
        toast.error("Vote Failed", {
          description: error.message,
        })
      }}
    />
  )
}

Custom Button Content

"use client"
 
import { AbstractVotingButton } from "@/components/abstract-voting-button"
 
export default function CustomContentVoting() {
  return (
    <AbstractVotingButton appId="1">
      🚀 Vote for this App
    </AbstractVotingButton>
  )
}

Using Hooks Directly

"use client";
 
import { useAccount } from "wagmi";
import { Button } from "@/components/ui/button";
import { useUserVoteStatus } from "@/hooks/use-user-vote-status";
import { useVoteForApp } from "@/hooks/use-vote-for-app";
 
const APP_ID = "15";
 
export default function CustomVotingImplementation() {
  const { isConnected } = useAccount();
 
  const { hasVoted, isLoading: statusLoading } = useUserVoteStatus({
    appId: APP_ID,
    enabled: isConnected,
  });
 
  const { voteForApp, isLoading: voteLoading } = useVoteForApp({
    onSuccess: (hash) => console.log("Vote submitted!", hash),
    onError: (error) => console.error("Vote failed:", error),
  });
 
  const isLoading = statusLoading || voteLoading;
 
  return (
    <Button onClick={() => voteForApp(APP_ID)} disabled={isLoading || hasVoted}>
      {isLoading ? "Loading..." : hasVoted ? "Voted" : "Vote"}
    </Button>
  );
}

What's included

This command installs the following files:

abstract-voting-button.tsx

A comprehensive voting button component with automatic wallet connection, vote status checking, and transaction submission support.

"use client";

import { useLoginWithAbstract } from "@abstract-foundation/agw-react";
import { Button } from "@/components/ui/button";
import { useAccount } from "wagmi";
import { cn } from "@/lib/utils";
import { type ClassValue } from "clsx";
import { toast } from "sonner";
import { abstract } from "viem/chains";
import { useUserVoteStatus } from "@/hooks/use-user-vote-status";
import { useVoteForApp } from "@/hooks/use-vote-for-app";

interface AbstractVotingButtonProps {
  appId: string | number | bigint;
  className?: ClassValue;
  children?: React.ReactNode;
  onVoteSuccess?: (data: `0x${string}`) => void;
  onVoteError?: (error: Error) => void;
  disabled?: boolean;
}

/**
 * Abstract Voting Button
 *
 * A voting button component that handles:
 * - Checking if user has already voted for an app
 * - Wallet connection via Abstract Global Wallet
 * - Submitting votes with proper loading states
 * - Error handling and user feedback
 */
export function AbstractVotingButton({
  appId,
  className,
  children,
  onVoteSuccess,
  onVoteError,
  disabled = false,
}: AbstractVotingButtonProps) {
  const { isConnected, chainId } = useAccount();
  const { login } = useLoginWithAbstract();

  // Check if user has already voted for this app
  const {
    hasVoted,
    isLoading: isStatusLoading,
  } = useUserVoteStatus({
    appId,
    enabled: isConnected,
  });

  const { voteForApp, isLoading: isVoteLoading } = useVoteForApp({
    onSuccess: (data) => {
      onVoteSuccess?.(data);
    },
    onError: (error) => {
      onVoteError?.(error);
    },
  });

  const isLoading = isStatusLoading || isVoteLoading;

  // Handle vote submission
  const handleVote = async () => {
    if (!isConnected) {
      login();
      return;
    }

    // Check if user is on testnet and show error toast
    if (chainId && chainId !== abstract.id) {
      toast.error(
        "App voting is only supported on Abstract mainnet. Please switch networks to vote."
      );
      return;
    }

    if (hasVoted || disabled) {
      return;
    }

    try {
      await voteForApp(appId);
    } catch (err) {
      // Error handling is done in the hook
    }
  };

  // Determine button text and state
  const getButtonContent = () => {
    if (children) {
      return children;
    }

    if (!isConnected) {
      return (
        <>
          Connect to Vote
          <AbstractLogo className="ml-2" />
        </>
      );
    }

    if (isLoading) {
      return (
        <>
          Loading...
          <AbstractLogo className="ml-2 animate-spin" />
        </>
      );
    }

    if (hasVoted) {
      return (
        <>
          Voted
          <CheckIcon className="ml-2 h-4 w-4" />
        </>
      );
    }

    return (
      <>
        Upvote on Abstract
        <VoteIcon className="ml-2 h-4 w-4" />
      </>
    );
  };

  const isButtonDisabled = disabled || isLoading || (isConnected && hasVoted);

  return (
    <Button
      onClick={handleVote}
      disabled={isButtonDisabled}
      className={cn(
        "relative transition-all duration-200",
        hasVoted && "bg-green-600 hover:bg-green-700 text-white",
        className
      )}
    >
      {/* Reserve width for the longest default label to prevent layout shift */}
      <span className="invisible whitespace-nowrap">
        Upvote on Abstract <VoteIcon className="ml-1 h-4 w-4" />
      </span>
      {/* Center the active content over the reserved space */}
      <span className="absolute inset-0 flex items-center justify-center">
        {getButtonContent()}
      </span>
    </Button>
  );
}

function VoteIcon({ className }: { className?: ClassValue }) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className={cn(className)}
    >
      <path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z" />
    </svg>
  );
}

function CheckIcon({ className }: { className?: ClassValue }) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className={cn(className)}
    >
      <path d="M20 6L9 17l-5-5" />
    </svg>
  );
}

function AbstractLogo({ className }: { className?: ClassValue }) {
  return (
    <svg
      width="20"
      height="18"
      viewBox="0 0 52 47"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className={cn(className)}
    >
      <path
        d="M33.7221 31.0658L43.997 41.3463L39.1759 46.17L28.901 35.8895C28.0201 35.0081 26.8589 34.5273 25.6095 34.5273C24.3602 34.5273 23.199 35.0081 22.3181 35.8895L12.0432 46.17L7.22205 41.3463L17.4969 31.0658H33.7141H33.7221Z"
        fill="currentColor"
      />
      <path
        d="M35.4359 28.101L49.4668 31.8591L51.2287 25.2645L37.1978 21.5065C35.9965 21.186 34.9954 20.4167 34.3708 19.335C33.7461 18.2613 33.586 17.0033 33.9063 15.8013L37.6623 1.76283L31.0713 0L27.3153 14.0385L35.4279 28.093L35.4359 28.101Z"
        fill="currentColor"
      />
      <path
        d="M15.7912 28.101L1.76028 31.8591L-0.00158691 25.2645L14.0293 21.5065C15.2306 21.186 16.2316 20.4167 16.8563 19.335C17.4809 18.2613 17.6411 17.0033 17.3208 15.8013L13.5648 1.76283L20.1558 0L23.9118 14.0385L15.7992 28.093L15.7912 28.101Z"
        fill="currentColor"
      />
    </svg>
  );
}

Props

PropTypeDefaultDescription
appIdstring | number | bigint-Required. The ID of the app to vote for
classNameClassValueundefinedCustom CSS classes
childrenReact.ReactNodeundefinedCustom button content
onVoteSuccess(data: any) => voidundefinedCallback when vote succeeds
onVoteError(error: Error) => voidundefinedCallback when vote fails
disabledbooleanfalseDisable the button

use-user-vote-status.ts

Hook that checks if the current user has already voted for a specific app in the current epoch.

"use client";

import { useAccount, useReadContract } from "wagmi";
import {
  ABSTRACT_VOTING_ADDRESS,
  ABSTRACT_VOTING_ABI,
} from "../lib/voting-contract";
import {
  hasUserVotedForApp,
  formatAppId,
  isValidAppId,
} from "../lib/voting-utils";

interface UseUserVoteStatusProps {
  appId: string | number | bigint;
  enabled?: boolean;
}

interface UserVoteStatus {
  hasVoted: boolean;
  isLoading: boolean;
  error: Error | null;
  refetch: () => void;
}

/**
 * Hook to check if the current user has voted for a specific app in the current epoch
 */
export function useUserVoteStatus({
  appId,
  enabled = true,
}: UseUserVoteStatusProps): UserVoteStatus {
  const { address, isConnected } = useAccount();

  // Get current epoch
  const { data: currentEpoch, isLoading: isEpochLoading } = useReadContract({
    address: ABSTRACT_VOTING_ADDRESS,
    abi: ABSTRACT_VOTING_ABI,
    functionName: "currentEpoch",
    query: {
      enabled: enabled && isConnected,
    },
  });

  // Get user votes for current epoch
  const {
    data: userVotes,
    isLoading: isVotesLoading,
    error,
    refetch,
  } = useReadContract({
    address: ABSTRACT_VOTING_ADDRESS,
    abi: ABSTRACT_VOTING_ABI,
    functionName: "getUserVotes",
    args: address && currentEpoch ? [address, currentEpoch] : undefined,
    query: {
      enabled:
        enabled &&
        isConnected &&
        !!address &&
        !!currentEpoch &&
        isValidAppId(appId),
    },
  });

  // Check if user has voted for this specific app
  const hasVoted = userVotes
    ? hasUserVotedForApp(userVotes, formatAppId(appId))
    : false;


  return {
    hasVoted,
    isLoading: isEpochLoading || isVotesLoading,
    error: error as Error | null,
    refetch,
  };
}

Parameters

ParameterTypeDefaultDescription
appIdstring | number | bigint-Required. The app ID to check
enabledbooleantrueWhether to enable the query

use-vote-for-app.ts

Hook that handles submitting votes for apps using Abstract Global Wallet.

"use client"

import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useAccount, useReadContract } from "wagmi"
import { useAbstractClient } from "@abstract-foundation/agw-react"
import { publicClient } from "@/config/viem-clients"
import { ABSTRACT_VOTING_ADDRESS, ABSTRACT_VOTING_ABI } from "../lib/voting-contract"
import { formatAppId, isValidAppId } from "../lib/voting-utils"

interface UseVoteForAppProps {
  onSuccess?: (data: `0x${string}`) => void
  onError?: (error: Error) => void
}

interface VoteForAppResult {
  voteForApp: (appId: string | number | bigint) => Promise<`0x${string}`>
  isLoading: boolean
  error: Error | null
  data: `0x${string}` | undefined
  reset: () => void
}

/**
 * Hook to submit a vote for an app using Abstract Global Wallet
 */
export function useVoteForApp({ onSuccess, onError }: UseVoteForAppProps = {}): VoteForAppResult {
  const { isConnected, address } = useAccount()
  const { data: abstractClient } = useAbstractClient()
  const queryClient = useQueryClient()

  // Get the current epoch to build the proper query key
  const { data: currentEpoch } = useReadContract({
    address: ABSTRACT_VOTING_ADDRESS,
    abi: ABSTRACT_VOTING_ABI,
    functionName: "currentEpoch",
    query: { enabled: false }, // Don't fetch, just get the query key structure
  })

  // Get the getUserVotes query key for invalidation
  const { queryKey } = useReadContract({
    address: ABSTRACT_VOTING_ADDRESS,
    abi: ABSTRACT_VOTING_ABI,
    functionName: "getUserVotes",
    args: address && currentEpoch ? [address, currentEpoch] : undefined,
    query: { enabled: false }, // Don't fetch, just get the query key
  })

  // Create mutation for voting
  const mutation = useMutation({
    mutationFn: async (appId: string | number | bigint) => {
      if (!isConnected) {
        throw new Error("Wallet not connected")
      }

      if (!abstractClient) {
        throw new Error("Abstract client not available")
      }

      if (!isValidAppId(appId)) {
        throw new Error(`Invalid app ID for voting. App ID: ${appId}`)
      }

      const formattedAppId = formatAppId(appId)

      // Submit the vote transaction using Abstract client
      const hash = await abstractClient.writeContract({
        address: ABSTRACT_VOTING_ADDRESS,
        abi: ABSTRACT_VOTING_ABI,
        functionName: "voteForApp",
        args: [formattedAppId],
      })

      // Wait for transaction to be confirmed on-chain
      await publicClient.waitForTransactionReceipt({ hash })

      return hash
    },
    onSuccess: async (data) => {
      // Invalidate getUserVotes queries using the proper wagmi query key
      await queryClient.invalidateQueries({ queryKey })
      
      onSuccess?.(data)
    },
    onError: (error: Error) => {
      onError?.(error)
    },
  })

  return {
    voteForApp: mutation.mutateAsync,
    isLoading: mutation.isPending,
    error: mutation.error as Error | null,
    data: mutation.data,
    reset: mutation.reset,
  }
}

Parameters

ParameterTypeDefaultDescription
onSuccess(data: any) => voidundefinedCallback when vote succeeds
onError(error: Error) => voidundefinedCallback when vote fails

Returns

PropertyTypeDescription
voteForApp(appId: string | number | bigint) => Promise<void>Function to submit a vote
isLoadingbooleanWhether a vote is being submitted
errorError | nullAny error that occurred
dataanyTransaction data from successful vote
reset() => voidReset the mutation state

voting-contract.ts

Contains the Abstract Voting contract address and ABI for both mainnet and testnet.

export const ABSTRACT_VOTING_ADDRESS = "0x3b50de27506f0a8c1f4122a1e6f470009a76ce2a" as const

export const ABSTRACT_VOTING_ABI = [{ "inputs": [{ "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "contract IAppRegistry", "name": "_appRegistry", "type": "address" }, { "internalType": "contract IVoteGovernor", "name": "_voteGovernor", "type": "address" }], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [], "name": "AlreadyInitialized", "type": "error" }, { "inputs": [{ "internalType": "uint256", "name": "appId", "type": "uint256" }], "name": "AlreadyVotedFor", "type": "error" }, { "inputs": [], "name": "AppNotActive", "type": "error" }, { "inputs": [], "name": "IndexOutOfBounds", "type": "error" }, { "inputs": [], "name": "InvalidSchedule", "type": "error" }, { "inputs": [], "name": "InvalidValue", "type": "error" }, { "inputs": [], "name": "NewOwnerIsZeroAddress", "type": "error" }, { "inputs": [], "name": "NoHandoverRequest", "type": "error" }, { "inputs": [], "name": "Unauthorized", "type": "error" }, { "inputs": [], "name": "UsedAllVotes", "type": "error" }, { "inputs": [], "name": "VotingNotActive", "type": "error" }, { "inputs": [], "name": "WithdrawFailed", "type": "error" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "newAppRegistry", "type": "address" }], "name": "AppRegistryUpdated", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "pendingOwner", "type": "address" }], "name": "OwnershipHandoverCanceled", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "pendingOwner", "type": "address" }], "name": "OwnershipHandoverRequested", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "oldOwner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" }], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "user", "type": "address" }, { "indexed": true, "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "RolesUpdated", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": false, "internalType": "uint256", "name": "startTime", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "epochDuration", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "epochsCompleted", "type": "uint256" }], "name": "ScheduleInitialized", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "newGovernor", "type": "address" }], "name": "VoteGovernorUpdated", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "voter", "type": "address" }, { "indexed": true, "internalType": "uint256", "name": "appId", "type": "uint256" }, { "indexed": true, "internalType": "uint256", "name": "epoch", "type": "uint256" }], "name": "Voted", "type": "event" }, { "inputs": [], "name": "MANAGER_ROLE", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "appRegistry", "outputs": [{ "internalType": "contract IAppRegistry", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "cancelOwnershipHandover", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "pendingOwner", "type": "address" }], "name": "completeOwnershipHandover", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "currentEpoch", "outputs": [{ "internalType": "uint256", "name": "epoch", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "currentSchedule", "outputs": [{ "internalType": "uint40", "name": "startTime", "type": "uint40" }, { "internalType": "uint40", "name": "epochDuration", "type": "uint40" }, { "internalType": "uint40", "name": "epochsCompleted", "type": "uint40" }, { "internalType": "uint96", "name": "voteCost", "type": "uint96" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }, { "internalType": "uint256", "name": "epoch", "type": "uint256" }], "name": "getUserVotes", "outputs": [{ "internalType": "uint256[]", "name": "", "type": "uint256[]" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "appId", "type": "uint256" }, { "internalType": "uint256", "name": "epoch", "type": "uint256" }], "name": "getVotesForApp", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }, { "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "grantRoles", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }, { "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "hasAllRoles", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }, { "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "hasAnyRole", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint40", "name": "_startTime", "type": "uint40" }, { "internalType": "uint40", "name": "_epochDuration", "type": "uint40" }, { "internalType": "uint96", "name": "_voteCost", "type": "uint96" }], "name": "initializeSchedule", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "nextSchedule", "outputs": [{ "internalType": "uint40", "name": "startTime", "type": "uint40" }, { "internalType": "uint40", "name": "epochDuration", "type": "uint40" }, { "internalType": "uint40", "name": "epochsCompleted", "type": "uint40" }, { "internalType": "uint96", "name": "voteCost", "type": "uint96" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [{ "internalType": "address", "name": "result", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "pendingOwner", "type": "address" }], "name": "ownershipHandoverExpiresAt", "outputs": [{ "internalType": "uint256", "name": "result", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "renounceRoles", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "requestOwnershipHandover", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }, { "internalType": "uint256", "name": "roles", "type": "uint256" }], "name": "revokeRoles", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "rolesOf", "outputs": [{ "internalType": "uint256", "name": "roles", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "newAppRegistry", "type": "address" }], "name": "setAppRegistry", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "newGovernor", "type": "address" }], "name": "setVoteGovernor", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], "name": "transferOwnership", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "userVoteSpend", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "userVotesRemaining", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "voteCost", "outputs": [{ "internalType": "uint96", "name": "", "type": "uint96" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint256", "name": "appId", "type": "uint256" }], "name": "voteForApp", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "voteGovernor", "outputs": [{ "internalType": "contract IVoteGovernor", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function" }] as const

voting-utils.ts

Utility functions for validating app IDs and checking vote status.

import { ABSTRACT_VOTING_ADDRESS, ABSTRACT_VOTING_ABI } from "./voting-contract"

/**
 * Custom error class for Abstract voting configuration issues.
 * These errors should bubble up to show helpful messages to developers.
 */
export class VotingConfigurationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "VotingConfigurationError";
  }
}

/**
 * Check if a user has voted for a specific app in the current epoch
 */
export function hasUserVotedForApp(userVotes: readonly bigint[], appId: bigint): boolean {
  return userVotes.some(vote => vote === appId)
}

/**
 * Format app ID to ensure it's a valid bigint
 */
export function formatAppId(appId: string | number | bigint): bigint {
  return BigInt(appId)
}

/**
 * Validate that an app ID is a positive number
 */
export function isValidAppId(appId: string | number | bigint): boolean {
  try {
    const id = BigInt(appId)
    return id > BigInt(0)
  } catch {
    return false
  }
}

export { ABSTRACT_VOTING_ADDRESS, ABSTRACT_VOTING_ABI }