Skip to main content
Glama

MPC Tally API Server

tally.service.ts14.8 kB
import { GraphQLClient } from "graphql-request"; import { getDAO } from "./organizations/getDAO.js"; import { listDAOs } from "./organizations/listDAOs.js"; import { listProposals } from "./proposals/listProposals.js"; import { getProposal } from "./proposals/getProposal.js"; import { getProposalVoters } from "./proposals/getProposalVoters.js"; import { getProposalTimeline } from "./proposals/getProposalTimeline.js"; import { getProposalSecurityAnalysis } from "./proposals/getProposalSecurityAnalysis.js"; import { listDelegates } from "./delegates/listDelegates.js"; import { getAddressProposals } from "./addresses/getAddressProposals.js"; import { getAddressDAOProposals } from "./addresses/getAddressDAOProposals.js"; import { getAddressVotes } from "./addresses/getAddressVotes.js"; import { getAddressCreatedProposals } from "./addresses/getAddressCreatedProposals.js"; import { getAddressMetadata } from "./addresses/getAddressMetadata.js"; import { getAddressGovernances } from "./addresses/getAddressGovernances.js"; import { getAddressReceivedDelegations } from "./addresses/getAddressReceivedDelegations.js"; import { getDelegateStatement } from "./delegates/getDelegateStatement.js"; import { getDelegators } from "./delegators/getDelegators.js"; import type { Organization, OrganizationsResponse, ListDAOsParams, PageInfo, Token, } from "./organizations/organizations.types.js"; import type { Delegate } from "./delegates/delegates.types.js"; import type { Delegation, GetDelegatorsParams, TokenInfo, } from "./delegators/delegators.types.js"; import type { GetAddressReceivedDelegationsInput } from "./addresses/addresses.types.js"; import type { DelegateStatement } from "./delegates/delegates.types.js"; import type { ProposalsInput, ProposalsResponse, ProposalInput, ProposalDetailsResponse, } from "./proposals/index.js"; import type { GetProposalVotersInput, ProposalVotersResponse, } from "./proposals/getProposalVoters.types.js"; import type { GetProposalTimelineInput, ProposalTimelineResponse, } from "./proposals/getProposalTimeline.types.js"; import type { GetProposalSecurityAnalysisInput, ProposalSecurityAnalysisResponse, } from "./proposals/getProposalSecurityAnalysis.types.js"; import type { AddressProposalsInput, AddressProposalsResponse, AddressDAOProposalsInput, AddressDAOProposalsResponse, AddressVotesInput, AddressVotesResponse, AddressCreatedProposalsInput, AddressCreatedProposalsResponse, AddressMetadataInput, AddressMetadataResponse, AddressGovernancesInput, AddressGovernancesResponse, } from "./addresses/addresses.types.js"; import { getDAOTokens } from "./organizations/getDAO.js"; import { getProposalVotesCast } from "./proposals/getProposalVotesCast.js"; import { getProposalVotesCastList } from "./proposals/getProposalVotesCastList.js"; import { getGovernanceProposalsStats } from "./proposals/getGovernanceProposalsStats.js"; import type { GetProposalVotesCastInput, ProposalVotesCastResponse, } from "./proposals/getProposalVotesCast.types.js"; import type { GetProposalVotesCastListInput, ProposalVotesCastListResponse, } from "./proposals/getProposalVotesCastList.types.js"; import type { GovernanceProposalsStatsResponse } from "./proposals/proposals.types.js"; import type { ListProposalsParams } from "./proposals/listProposals.types.js"; import type { ListDelegatesParams } from "./delegates/delegates.types.js"; export interface TallyServiceConfig { apiKey: string; baseUrl?: string; } export interface GetAddressReceivedDelegationsOutput { nodes: Array<{ id: string; votes: string; delegator: { id: string; address: string; }; }>; pageInfo: { firstCursor: string | null; lastCursor: string | null; count: number; }; totalCount: number; } export type GetDelegateStatementInput = { address: string; } & ( | { governorId: string; organizationSlug?: never } | { organizationSlug: string; governorId?: never } ); export class TallyService { private client: GraphQLClient; constructor(config: TallyServiceConfig) { this.client = new GraphQLClient( config.baseUrl || "https://api.tally.xyz/query", { headers: { "Content-Type": "application/json", "api-key": config.apiKey, }, } ); } async listProposals(params: ListProposalsParams): Promise<ProposalsResponse> { return listProposals(this.client, params); } async getDAO(slug: string): Promise<Organization> { const { organization } = await getDAO(this.client, slug); return { id: organization.id, name: organization.name, slug: organization.slug, chainIds: organization.chainIds, tokenIds: organization.tokenIds, governorIds: organization.governorIds, tokenOwnersCount: organization.tokenOwnersCount, delegatesCount: organization.delegatesCount, proposalsCount: organization.proposalsCount, hasActiveProposals: organization.hasActiveProposals, metadata: organization.metadata, delegatesVotesCount: organization.delegatesVotesCount || 0, }; } async getDAOTokens(tokenIds: string[]): Promise<Token[]> { return getDAOTokens(this.client, tokenIds); } async listDAOs(params: ListDAOsParams = {}): Promise<OrganizationsResponse> { return listDAOs(this.client, params); } async listDelegates(input: ListDelegatesParams) { if (!input.organizationSlug) { throw new Error("organizationSlug must be a string"); } return listDelegates(this.client, input); } async getProposal(input: ProposalInput): Promise<ProposalDetailsResponse> { return getProposal(this.client, input); } async getProposalVoters( input: GetProposalVotersInput ): Promise<ProposalVotersResponse> { if (!input.proposalId) { throw new Error("proposalId is required"); } return getProposalVoters(this.client, input); } async getProposalTimeline( input: GetProposalTimelineInput ): Promise<ProposalTimelineResponse> { if (!input.proposalId) { throw new Error("proposalId is required"); } return getProposalTimeline(this.client, input); } async getProposalSecurityAnalysis( input: GetProposalSecurityAnalysisInput ): Promise<ProposalSecurityAnalysisResponse> { if (!input.proposalId) { throw new Error("proposalId is required"); } return getProposalSecurityAnalysis(this.client, input); } async getAddressProposals( input: AddressProposalsInput ): Promise<AddressProposalsResponse> { if (!input.address) { throw new Error("address is required"); } return getAddressProposals(this.client, input); } async getAddressDAOProposals( input: AddressDAOProposalsInput ): Promise<AddressDAOProposalsResponse> { if (!input.address) { throw new Error("Address is required"); } const response = await getAddressDAOProposals(this.client, input); return { proposals: { nodes: response.proposals?.nodes || [], pageInfo: response.proposals?.pageInfo || { firstCursor: null, lastCursor: null, }, }, }; } async getAddressVotes( input: AddressVotesInput ): Promise<AddressVotesResponse> { return getAddressVotes(this.client, input); } async getAddressCreatedProposals( input: AddressCreatedProposalsInput ): Promise<AddressCreatedProposalsResponse> { if (!input.address) { throw new Error("address is required"); } const response = await getAddressCreatedProposals(this.client, input); return { proposals: { nodes: response.proposals?.nodes || [], pageInfo: response.proposals?.pageInfo || { firstCursor: null, lastCursor: null, }, }, }; } async getAddressMetadata( input: AddressMetadataInput ): Promise<AddressMetadataResponse> { if (!input.address) { throw new Error("Address is required"); } const response = await getAddressMetadata(this.client, input); return { address: response.address?.address || input.address, accounts: response.address?.accounts || [], }; } async getAddressGovernances( input: AddressGovernancesInput ): Promise<Record<string, any>> { return await getAddressGovernances(this.client, input); } async getAddressReceivedDelegations( input: GetAddressReceivedDelegationsInput ): Promise<GetAddressReceivedDelegationsOutput> { if (!input.address) { throw new Error("address is required"); } return getAddressReceivedDelegations(this.client, input); } async getDelegateStatement( input: GetDelegateStatementInput ): Promise<DelegateStatement | null> { const response = await getDelegateStatement(this.client, input); if (!response?.statement) return null; return { id: response.statement.id, address: response.statement.address, statement: response.statement.statement, statementSummary: response.statement.statementSummary || "", isSeekingDelegation: response.statement.isSeekingDelegation || false, issues: response.statement.issues || [], }; } async getDelegators(params: GetDelegatorsParams): Promise<{ delegators: Delegation[]; pageInfo: PageInfo; }> { if (!params.address) { throw new Error("address is required"); } return getDelegators(this.client, params); } async getProposalVotesCast( input: GetProposalVotesCastInput ): Promise<ProposalVotesCastResponse> { if (!input.id) { throw new Error("proposalId is required"); } return getProposalVotesCast(this.client, input); } async getProposalVotesCastList( input: GetProposalVotesCastListInput ): Promise<ProposalVotesCastListResponse> { return getProposalVotesCastList(this.client, input); } async getGovernanceProposalsStats(input: { slug: string; }): Promise<GovernanceProposalsStatsResponse> { return getGovernanceProposalsStats(this.client, input); } /** * Format a vote amount considering token decimals * @param {string} votes - The raw vote amount * @param {TokenInfo} token - Optional token info containing decimals and symbol * @returns {string} Formatted vote amount with optional symbol */ private static formatVotes(votes: string, token?: TokenInfo): string { const val = BigInt(votes); const decimals = token?.decimals ?? 18; const denominator = BigInt(10 ** decimals); const formatted = (Number(val) / Number(denominator)).toLocaleString(); return `${formatted}${token?.symbol ? ` ${token.symbol}` : ""}`; } static formatDAOList(daos: Organization[]): string { return ( `Found ${daos.length} DAOs:\n\n` + daos .map( (dao) => `${dao.name} (${dao.slug})\n` + `Token Holders: ${dao.tokenOwnersCount}\n` + `Delegates: ${dao.delegatesCount}\n` + `Proposals: ${dao.proposalsCount}\n` + `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` + `Description: ${ dao.metadata?.description || "No description available" }\n` + `Website: ${dao.metadata?.socials?.website || "N/A"}\n` + `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` + `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` + "---" ) .join("\n\n") ); } static formatDAO(dao: Organization): string { return ( `${dao.name} (${dao.slug})\n` + `Token Holders: ${dao.tokenOwnersCount}\n` + `Delegates: ${dao.delegatesCount}\n` + `Proposals: ${dao.proposalsCount}\n` + `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` + `Description: ${ dao.metadata?.description || "No description available" }\n` + `Website: ${dao.metadata?.socials?.website || "N/A"}\n` + `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` + `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` + `Chain IDs: ${dao.chainIds.join(", ")}\n` + `Token IDs: ${dao.tokenIds?.join(", ") || "N/A"}\n` + `Governor IDs: ${dao.governorIds?.join(", ") || "N/A"}` ); } static formatDelegatesList(delegates: Delegate[]): string { return ( `Found ${delegates.length} delegates:\n\n` + delegates .map( (delegate) => `${delegate.account.name || delegate.account.address}\n` + `Address: ${delegate.account.address}\n` + `Votes: ${delegate.votesCount}\n` + `Delegators: ${delegate.delegatorsCount}\n` + `Bio: ${delegate.account.bio || "No bio available"}\n` + `Statement: ${ delegate.statement?.statementSummary || "No statement available" }\n` + "---" ) .join("\n\n") ); } static formatDelegatorsList(delegators: Delegation[]): string { return ( `Found ${delegators.length} delegators:\n\n` + delegators .map( (delegation) => `${ delegation.delegator.name || delegation.delegator.ens || delegation.delegator.address }\n` + `Address: ${delegation.delegator.address}\n` + `Votes: ${TallyService.formatVotes( delegation.votes, delegation.token )}\n` + `Delegated at: Block ${delegation.blockNumber} (${new Date( delegation.blockTimestamp ).toLocaleString()})\n` + `${ delegation.token ? `Token: ${delegation.token.symbol} (${delegation.token.name})\n` : "" }` + "---" ) .join("\n\n") ); } static formatProposal(proposal: any): string { return `Proposal: ${proposal.metadata.title} ID: ${proposal.id} Status: ${proposal.status} Created: ${new Date(proposal.createdAt).toLocaleString()} Description: ${proposal.metadata.description} Governor: ${proposal.governor.name} Vote Stats: ${proposal.voteStats .map( (stat: any) => ` ${stat.type}: ${stat.percent.toFixed(2)}% (${ stat.votesCount } votes from ${stat.votersCount} voters)` ) .join("\n")}`; } static formatProposalsList(proposals: any[]): string { return ( `Found ${proposals.length} proposals:\n\n` + proposals .map( (proposal) => `${proposal.metadata.title}\n` + `Tally ID: ${proposal.id}\n` + `Status: ${proposal.status}\n` + `Created: ${new Date(proposal.createdAt).toLocaleString()}\n\n` ) .join("") ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/crazyrabbitLTC/mpc-tally-api-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server