@moonwell-fi/moonwell-sdk
Version:
TypeScript Interface for Moonwell
467 lines (388 loc) • 12.2 kB
text/typescript
import type { Environment } from "../../environments/index.js";
import { getWithRetry } from "../axiosWithRetry.js";
/**
* Base configuration for Governor API requests
* The Governor API is the new governance indexer, accessed via governanceIndexerUrl
*/
const getGovernorApiUrl = (environment: Environment): string => {
return environment.governanceIndexerUrl;
};
/**
* Paginated response type
*/
export type PaginatedResponse<T> = {
results: T[];
nextCursor?: string;
};
/**
* Pagination options
*/
export type PaginationOptions = {
limit?: number;
cursor?: string | undefined;
};
/**
* Raw API response types matching Governor API spec
*/
export type ApiProposal = {
id: string;
chainId: number;
proposalId: number;
proposer: string;
targets: string[];
values: string[];
calldatas: string[];
votingStartTime: number;
votingEndTime: number;
description: string;
forVotes: string;
againstVotes: string;
abstainVotes: string;
blockNumber: string;
timestamp: number;
transactionHash: string;
stateChanges?: ApiProposalStateChange[];
};
export type ApiVote = {
id: string;
proposalId: string;
voter: string;
votes: string; // Raw vote weight (NOT scaled)
voteValue: number; // 0=for, 1=against, 2=abstain
blockNumber: string;
chainId: number;
timestamp: number;
proposalForVotes: string;
proposalAgainstVotes: string;
proposalAbstainVotes: string;
};
export type ApiVoter = {
id: string;
firstSeenBlock: string;
firstSeenTimestamp: number;
votedProposalIds: string[];
createdProposalIds: string[];
};
export type ApiProposalStateChange = {
id: string;
proposalId: string;
state: "CREATED" | "CANCELED" | "EXECUTED" | "QUEUED" | "REBROADCASTED";
blockNumber: string;
timestamp: number;
transactionHash: string;
forVotes: string;
againstVotes: string;
abstainVotes: string;
chainId: number; // Chain ID where this state change occurred
};
export type ApiVoteReceipt = {
id: string;
proposalId: string;
voter: string;
votes: string;
voteValue: number; // 0=for, 1=against, 2=abstain
blockNumber: string;
chainId: number;
timestamp: number;
};
/**
* Fetch proposals from Governor API
*/
export async function fetchProposals(
environment: Environment,
options?: PaginationOptions & { chainId?: number },
): Promise<PaginatedResponse<ApiProposal>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
if (options?.chainId) params.append("chainId", options.chainId.toString());
const response = await getWithRetry<PaginatedResponse<ApiProposal>>(
`${baseUrl}/api/v1/governor/proposals?${params.toString()}`,
);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch proposals: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch all proposals (handles pagination internally)
*/
export async function fetchAllProposals(
environment: Environment,
options?: { chainId?: number },
): Promise<ApiProposal[]> {
const allProposals: ApiProposal[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchProposals(environment, {
limit: 1000,
...(cursor && { cursor }),
...(options?.chainId && { chainId: options.chainId }),
});
allProposals.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allProposals;
}
/**
* Fetch a single proposal from Governor API
*/
export async function fetchProposal(
environment: Environment,
proposalId: string,
): Promise<ApiProposal> {
const baseUrl = getGovernorApiUrl(environment);
const response = await getWithRetry<ApiProposal>(
`${baseUrl}/api/v1/governor/proposals/${proposalId}`,
);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch proposal: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch votes for a proposal
*/
export async function fetchProposalVotes(
environment: Environment,
proposalId: string,
options?: PaginationOptions,
): Promise<PaginatedResponse<ApiVote>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
const response = await getWithRetry<PaginatedResponse<ApiVote>>(
`${baseUrl}/api/v1/governor/proposals/${proposalId}/votes?${params.toString()}`,
);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch proposal votes: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch all votes for a proposal (handles pagination internally)
*/
export async function fetchAllProposalVotes(
environment: Environment,
proposalId: string,
): Promise<ApiVote[]> {
const allVotes: ApiVote[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchProposalVotes(environment, proposalId, {
limit: 1000,
...(cursor && { cursor }),
});
allVotes.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allVotes;
}
/**
* Fetch state changes for a proposal
*/
export async function fetchProposalStateChanges(
environment: Environment,
proposalId: string,
options?: PaginationOptions,
): Promise<PaginatedResponse<ApiProposalStateChange>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
const response = await getWithRetry<
PaginatedResponse<ApiProposalStateChange>
>(
`${baseUrl}/api/v1/governor/proposals/${proposalId}/state-changes?${params.toString()}`,
);
if (response.status !== 200 || !response.data) {
throw new Error(
`Failed to fetch proposal state changes: ${response.statusText}`,
);
}
return response.data;
}
/**
* Fetch all state changes for a proposal (handles pagination internally)
*/
export async function fetchAllProposalStateChanges(
environment: Environment,
proposalId: string,
): Promise<ApiProposalStateChange[]> {
const allStateChanges: ApiProposalStateChange[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchProposalStateChanges(environment, proposalId, {
limit: 1000,
...(cursor && { cursor }),
});
allStateChanges.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allStateChanges;
}
/**
* Fetch all voters (delegates)
*/
export async function fetchVoters(
environment: Environment,
options?: PaginationOptions,
): Promise<PaginatedResponse<ApiVoter>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
const endpoint = `${baseUrl}/api/v1/governor/voters?${params.toString()}`;
try {
const response = await getWithRetry<PaginatedResponse<ApiVoter>>(endpoint);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch voters: ${response.statusText}`);
}
return response.data;
} catch (error: any) {
console.error("[Governor API] CORS/Network Error:", {
message: error.message,
code: error.code,
endpoint,
error: error.response || error,
});
throw new Error(
`CORS or Network error when fetching voters. The API at ${baseUrl} may need CORS headers configured. Original error: ${error.message}`,
);
}
}
/**
* Fetch all voters (handles pagination internally)
*/
export async function fetchAllVoters(
environment: Environment,
): Promise<ApiVoter[]> {
const allVoters: ApiVoter[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchVoters(environment, {
limit: 100,
...(cursor && { cursor }),
});
allVoters.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allVoters;
}
/**
* Fetch a single voter
*/
export async function fetchVoter(
environment: Environment,
address: string,
): Promise<ApiVoter> {
const baseUrl = getGovernorApiUrl(environment);
const response = await getWithRetry<ApiVoter>(
`${baseUrl}/api/v1/governor/voters/${address}`,
);
if (response.status === 404) {
throw new Error("Voter not found");
}
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch voter: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch proposals created by a specific address
*/
export async function fetchVoterProposals(
environment: Environment,
address: string,
options?: PaginationOptions,
): Promise<PaginatedResponse<ApiProposal>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
const endpoint = `${baseUrl}/api/v1/governor/voters/${address}/proposals?${params.toString()}`;
const response = await getWithRetry<PaginatedResponse<ApiProposal>>(endpoint);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch voter proposals: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch all proposals created by a specific address (handles pagination internally)
*/
export async function fetchAllVoterProposals(
environment: Environment,
address: string,
): Promise<ApiProposal[]> {
const allProposals: ApiProposal[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchVoterProposals(environment, address, {
limit: 1000,
...(cursor && { cursor }),
});
allProposals.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allProposals;
}
/**
* Fetch votes cast by a specific address
*/
export async function fetchVoterVotes(
environment: Environment,
address: string,
options?: PaginationOptions,
): Promise<PaginatedResponse<ApiVote>> {
const baseUrl = getGovernorApiUrl(environment);
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.cursor) params.append("cursor", options.cursor);
const endpoint = `${baseUrl}/api/v1/governor/voters/${address}/votes?${params.toString()}`;
const response = await getWithRetry<PaginatedResponse<ApiVote>>(endpoint);
if (response.status !== 200 || !response.data) {
throw new Error(`Failed to fetch voter votes: ${response.statusText}`);
}
return response.data;
}
/**
* Fetch all votes cast by a specific address (handles pagination internally)
*/
export async function fetchAllVoterVotes(
environment: Environment,
address: string,
): Promise<ApiVote[]> {
const allVotes: ApiVote[] = [];
let cursor: string | undefined = undefined;
do {
const response = await fetchVoterVotes(environment, address, {
limit: 1000,
...(cursor && { cursor }),
});
allVotes.push(...response.results);
cursor = response.nextCursor;
} while (cursor);
return allVotes;
}
/**
* Fetch vote receipts for a specific user on a proposal
* Returns all votes across all chains for multichain proposals
*/
export async function fetchUserVoteReceipt(
environment: Environment,
proposalId: string,
voterAddress: string,
): Promise<ApiVoteReceipt[]> {
const baseUrl = getGovernorApiUrl(environment);
const response = await getWithRetry<ApiVoteReceipt[]>(
`${baseUrl}/api/v1/governor/proposals/${proposalId}/vote/${voterAddress}`,
);
if (response.status !== 200 || !response.data) {
throw new Error(
`Failed to fetch user vote receipt: ${response.statusText}`,
);
}
return response.data;
}