diff --git a/src/data/formulas/contract/prePropose/daoPreProposeApprovalSingle.ts b/src/data/formulas/contract/prePropose/daoPreProposeApprovalSingle.ts index de780b71..2893d45a 100644 --- a/src/data/formulas/contract/prePropose/daoPreProposeApprovalSingle.ts +++ b/src/data/formulas/contract/prePropose/daoPreProposeApprovalSingle.ts @@ -1,63 +1,360 @@ -import { ContractFormula } from '@/core' +import { ContractEnv, ContractFormula } from '@/core' export * from './daoPreProposeBase' +type ProposalStatus = + | { + pending: {} + } + | { + approved: { + created_proposal_id: number + } + } + | { + rejected: {} + } + +type Proposal = { + status: ProposalStatus + approval_id: number + proposer: string + msg: any + deposit: any + // Extra. + createdAt?: string + completedAt?: string +} + export const approver: ContractFormula = { compute: async ({ contractAddress, get }) => await get(contractAddress, 'approver'), } -export const pendingProposal: ContractFormula = - { - compute: async ({ contractAddress, get, args: { id } }) => { - if (!id) { - throw new Error('missing `id`') - } +export const proposalCreatedAt: ContractFormula< + string | undefined, + { id: string } +> = { + compute: async ({ + contractAddress, + getDateFirstTransformed, + getDateKeyFirstSet, + args: { id }, + }) => + ( + (await getDateFirstTransformed( + contractAddress, + `pendingProposal:${id}` + )) ?? + // Fallback to events. + (await getDateKeyFirstSet( + contractAddress, + 'pending_proposals', + Number(id) + )) + )?.toISOString(), +} - return await get(contractAddress, 'pending_proposals', id) - }, - } +export const proposalCompletedAt: ContractFormula< + string | undefined, + { id: string } +> = { + compute: async ({ + contractAddress, + getDateFirstTransformed, + getDateKeyFirstSet, + args: { id }, + }) => + ( + (await getDateFirstTransformed( + contractAddress, + `completedProposal:${id}` + )) ?? + // Fallback to events. + (await getDateKeyFirstSet( + contractAddress, + 'completed_proposals', + Number(id) + )) + )?.toISOString(), +} + +export const proposal: ContractFormula = { + compute: async (env) => { + const { + contractAddress, + getTransformationMatch, + get, + args: { id }, + } = env + + if (!id) { + throw new Error('missing `id`') + } + + const idNum = Number(id) + let proposal = + ( + await getTransformationMatch( + contractAddress, + `completedProposal:${id}` + ) + )?.value || + ( + await getTransformationMatch( + contractAddress, + `pendingProposal:${id}` + ) + )?.value || + (await get(contractAddress, 'completed_proposals', idNum)) || + (await get(contractAddress, 'pending_proposals', idNum)) + + return proposal && (await withMetadata(env, proposal)) + }, +} -export const pendingProposals: ContractFormula = { - compute: async ({ contractAddress, getMap }) => { - const pendingProposals = await getMap( +export const pendingProposals: ContractFormula< + Proposal[], + { + limit?: string + startAfter?: string + } +> = { + compute: async (env) => { + const { contractAddress, - 'pending_proposals', - { + getTransformationMap, + getMap, + args: { limit, startAfter }, + } = env + + const pendingProposals = + (await getTransformationMap( + contractAddress, + 'pendingProposal' + )) || + (await getMap(contractAddress, 'pending_proposals', { keyType: 'number', - } + })) + + if (!pendingProposals) { + return [] + } + + const limitNum = limit ? Math.max(0, Number(limit)) : Infinity + const startAfterNum = startAfter + ? Math.max(0, Number(startAfter)) + : -Infinity + + const proposalIds = Object.keys(pendingProposals) + .map(Number) + // Ascending by proposal ID. + .sort((a, b) => a - b) + .filter((id) => id > startAfterNum) + .slice(0, limitNum) + + return await Promise.all( + proposalIds.map((id) => withMetadata(env, pendingProposals[id])) ) + }, +} + +export const reversePendingProposals: ContractFormula< + Proposal[], + { + limit?: string + startBefore?: string + } +> = { + compute: async (env) => { + const { + contractAddress, + getTransformationMap, + getMap, + args: { limit, startBefore }, + } = env + + const pendingProposals = + (await getTransformationMap( + contractAddress, + 'pendingProposal' + )) || + (await getMap(contractAddress, 'pending_proposals', { + keyType: 'number', + })) + if (!pendingProposals) { - return undefined + return [] } - return ( - Object.entries(pendingProposals) - // Descending by ID. - .sort((a, b) => Number(b[0]) - Number(a[0])) - .map(([, proposal]) => proposal) + const limitNum = limit ? Math.max(0, Number(limit)) : Infinity + const startBeforeNum = startBefore + ? Math.max(0, Number(startBefore)) + : Infinity + + const proposalIds = Object.keys(pendingProposals) + .map(Number) + // Descending by proposal ID. + .sort((a, b) => b - a) + .filter((id) => id < startBeforeNum) + .slice(0, limitNum) + + return await Promise.all( + proposalIds.map((id) => withMetadata(env, pendingProposals[id])) ) }, } -export const reversePendingProposals: ContractFormula = { - compute: async ({ contractAddress, getMap }) => { - const pendingProposals = await getMap( +export const completedProposals: ContractFormula< + Proposal[], + { + limit?: string + startAfter?: string + } +> = { + compute: async (env) => { + const { contractAddress, - 'pending_proposals', - { + getTransformationMap, + getMap, + args: { limit, startAfter }, + } = env + + const completedProposals = + (await getTransformationMap( + contractAddress, + 'completedProposal' + )) || + (await getMap(contractAddress, 'completed_proposals', { keyType: 'number', - } + })) + + if (!completedProposals) { + return [] + } + + const limitNum = limit ? Math.max(0, Number(limit)) : Infinity + const startAfterNum = startAfter + ? Math.max(0, Number(startAfter)) + : -Infinity + + const proposalIds = Object.keys(completedProposals) + .map(Number) + // Ascending by proposal ID. + .sort((a, b) => a - b) + .filter((id) => id > startAfterNum) + .slice(0, limitNum) + + return await Promise.all( + proposalIds.map((id) => withMetadata(env, completedProposals[id])) ) - if (!pendingProposals) { - return undefined + }, +} + +export const reverseCompletedProposals: ContractFormula< + Proposal[], + { + limit?: string + startBefore?: string + } +> = { + compute: async (env) => { + const { + contractAddress, + getTransformationMap, + getMap, + args: { limit, startBefore }, + } = env + + const completedProposals = + (await getTransformationMap( + contractAddress, + 'completedProposal' + )) || + (await getMap(contractAddress, 'completed_proposals', { + keyType: 'number', + })) + + if (!completedProposals) { + return [] + } + + const limitNum = limit ? Math.max(0, Number(limit)) : Infinity + const startBeforeNum = startBefore + ? Math.max(0, Number(startBefore)) + : Infinity + + const proposalIds = Object.keys(completedProposals) + .map(Number) + // Descending by proposal ID. + .sort((a, b) => b - a) + .filter((id) => id < startBeforeNum) + .slice(0, limitNum) + + return await Promise.all( + proposalIds.map((id) => withMetadata(env, completedProposals[id])) + ) + }, +} + +export const completedProposalIdForCreatedProposalId: ContractFormula< + number | undefined, + { + id: string + } +> = { + compute: async ({ + contractAddress, + getTransformationMatch, + get, + args: { id }, + }) => { + if (!id) { + throw new Error('missing `id`') } return ( - Object.entries(pendingProposals) - // Ascending by ID. - .sort((a, b) => Number(a[0]) - Number(b[0])) - .map(([, proposal]) => proposal) + ( + await getTransformationMatch( + contractAddress, + `createdToCompletedProposal:${id}` + ) + )?.value || + // Fallback to events. + (await get( + contractAddress, + 'created_to_completed_proposal', + Number(id) + )) ) }, } + +// Helpers + +const withMetadata = async ( + env: ContractEnv, + proposal: Proposal +): Promise => { + const [createdAt, completedAt] = await Promise.all([ + proposalCreatedAt.compute({ + ...env, + args: { + id: proposal.approval_id.toString(), + }, + }), + proposalCompletedAt.compute({ + ...env, + args: { + id: proposal.approval_id.toString(), + }, + }), + ]) + + return { + ...proposal, + // Extra. + createdAt, + completedAt, + } +} diff --git a/src/data/transformers/prePropose/daoPreProposeApprovalSingle.ts b/src/data/transformers/prePropose/daoPreProposeApprovalSingle.ts index a6585bbb..9c3fad0c 100644 --- a/src/data/transformers/prePropose/daoPreProposeApprovalSingle.ts +++ b/src/data/transformers/prePropose/daoPreProposeApprovalSingle.ts @@ -1,9 +1,13 @@ import { Transformer } from '@/core/types' -import { dbKeyForKeys } from '@/core/utils' +import { dbKeyForKeys, dbKeyToKeys } from '@/core/utils' + +import { makeTransformerForMap } from '../utils' const CODE_IDS_KEYS: string[] = ['dao-pre-propose-approval-single'] const KEY_APPROVER = dbKeyForKeys('approver') +const KEY_PREFIX_PENDING_PROPOSALS = dbKeyForKeys('pending_proposals', '') +const KEY_PREFIX_COMPLETED_PROPOSALS = dbKeyForKeys('completed_proposals', '') const approver: Transformer = { filter: { @@ -14,4 +18,71 @@ const approver: Transformer = { getValue: async () => true, } -export default [approver] +const pendingProposal: Transformer = { + filter: { + codeIdsKeys: CODE_IDS_KEYS, + matches: (event) => event.key.startsWith(KEY_PREFIX_PENDING_PROPOSALS), + }, + name: (event) => { + // "pending_proposals", proposalId + const [, proposalId] = dbKeyToKeys(event.key, [false, true]) + return `pendingProposal:${proposalId}` + }, + getValue: (event) => event.valueJson, +} + +const completedProposal: Transformer = { + filter: { + codeIdsKeys: CODE_IDS_KEYS, + matches: (event) => event.key.startsWith(KEY_PREFIX_COMPLETED_PROPOSALS), + }, + name: (event) => { + // "completed_proposals", proposalId + const [, proposalId] = dbKeyToKeys(event.key, [false, true]) + return `completedProposal:${proposalId}` + }, + getValue: (event) => event.valueJson, +} + +const proposed: Transformer = { + filter: { + codeIdsKeys: CODE_IDS_KEYS, + matches: (event) => + event.key.startsWith(KEY_PREFIX_PENDING_PROPOSALS) && + !!event.valueJson?.proposer && + event.valueJson?.status && + 'pending' in event.valueJson.status, + }, + name: (event) => { + // Ignore deletes. Can't transform if we can't access the proposer. + if (event.delete || !event.valueJson?.proposer) { + return + } + + // "pending_proposals"|"completed_proposals", proposalId + const [, proposalId] = dbKeyToKeys(event.key, [false, true]) + return `proposed:${event.valueJson.proposer}:${proposalId}` + }, + getValue: (event) => { + // "pending_proposals"|"completed_proposals", proposalId + const [, proposalId] = dbKeyToKeys(event.key, [false, true]) + return { proposalId } + }, +} + +const createdToCompletedProposal = makeTransformerForMap( + CODE_IDS_KEYS, + 'createdToCompletedProposal', + 'created_to_completed_proposal', + { + numericKey: true, + } +) + +export default [ + approver, + pendingProposal, + completedProposal, + proposed, + createdToCompletedProposal, +]