Skip to content

Commit

Permalink
added gov proposal webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Apr 22, 2024
1 parent 628a2e7 commit 2d8cb51
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 23 deletions.
57 changes: 37 additions & 20 deletions src/data/webhooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import { State, WasmStateEvent } from '@/db'
import { makeProposalCreated } from './discordNotifier'
import { makeIndexerCwReceiptPaid } from './indexerCwReceipt'
import { makeInboxJoinedDao } from './notify/dao'
import {
makeInboxGovProposalCreated,
makeInboxGovProposalPassed,
makeInboxGovProposalRejected,
} from './notify/gov'
import {
makeInboxPreProposeApprovalProposalCreated,
makeInboxPreProposeApprovalProposalRejected,
makeInboxProposalClosed,
makeInboxProposalCreated,
makeInboxProposalExecuted,
} from './notify/proposal'
import { makeBroadcastVoteCast, makeProposalStatusChanged } from './websockets'
import {
makeBroadcastVoteCast,
makeDaoProposalStatusChanged,
makeGovProposalStatusChanged,
} from './websockets'

let processedWebhooks: ProcessedWebhook<any, any>[] | undefined
export const getProcessedWebhooks = (
Expand All @@ -30,9 +39,13 @@ export const getProcessedWebhooks = (
makeInboxProposalClosed,
makeInboxPreProposeApprovalProposalCreated,
makeInboxPreProposeApprovalProposalRejected,
makeInboxGovProposalCreated,
makeInboxGovProposalPassed,
makeInboxGovProposalRejected,
makeIndexerCwReceiptPaid,
makeBroadcastVoteCast,
makeProposalStatusChanged,
makeDaoProposalStatusChanged,
makeGovProposalStatusChanged,
]

const _webhooks: Webhook[] = [
Expand Down Expand Up @@ -76,26 +89,30 @@ export const getProcessedWebhooks = (
}
}

// Wrap in try/catch in case a webhook errors. Don't want to prevent
// other webhooks from sending.
try {
return filter.matches?.(event) ?? true
} catch (error) {
console.error(
`Error matching webhook for ${event.constructor.name} ID ${event.id} at height ${event.block.height}: ${error}`
)
Sentry.captureException(error, {
tags: {
type: 'failed-webhook-match',
},
extra: {
event,
},
})
if (filter.matches) {
// Wrap in try/catch in case a webhook errors. Don't want to prevent
// other webhooks from sending.
try {
return filter.matches(event)
} catch (error) {
console.error(
`Error matching webhook for ${event.constructor.name} ID ${event.id} at height ${event.block.height}: ${error}`
)
Sentry.captureException(error, {
tags: {
type: 'failed-webhook-match',
},
extra: {
event,
},
})

// On error, do not match.
return false
// On error, do not match.
return false
}
}

return true
},
}
})
Expand Down
151 changes: 151 additions & 0 deletions src/data/webhooks/notify/gov.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ProposalStatus } from '@dao-dao/types/protobuf/codegen/cosmos/gov/v1/gov'
import {
getConfiguredChainConfig,
getDisplayNameForChainId,
getImageUrlForChainId,
} from '@dao-dao/utils'

import { WebhookMaker, WebhookType } from '@/core/types'
import { decodeGovProposal } from '@/core/utils'
import { GovStateEvent } from '@/db'

// Fire webhook when a gov proposal is created.
export const makeInboxGovProposalCreated: WebhookMaker<GovStateEvent> = (
config,
state
) => ({
filter: {
EventType: GovStateEvent,
},
endpoint: {
type: WebhookType.Url,
url: 'https://notifier.daodao.zone/notify',
method: 'POST',
headers: {
'x-api-key': config.notifierSecret,
},
},
getValue: async (event, getLastEvent) => {
// Only fire the webhook the first time this exists.
if ((await getLastEvent()) !== null) {
return
}

const { proposal, title } = decodeGovProposal(event.data)

return {
chainId: state.chainId,
type: 'proposal_created',
data: {
chainId: state.chainId,
dao: getConfiguredChainConfig(state.chainId)?.name || 'GOV_PLACEHOLDER',
daoName: getDisplayNameForChainId(state.chainId),
imageUrl: getImageUrlForChainId(state.chainId),
proposalId: event.proposalId,
proposalTitle: proposal ? title : event.proposalId,
fromApprover: false,
},
}
},
})

// Fire webhook when a gov proposal is passed (or passed + execution failed).
export const makeInboxGovProposalPassed: WebhookMaker<GovStateEvent> = (
config,
state
) => ({
filter: {
EventType: GovStateEvent,
},
endpoint: {
type: WebhookType.Url,
url: 'https://notifier.daodao.zone/notify',
method: 'POST',
headers: {
'x-api-key': config.notifierSecret,
},
},
getValue: async (event, getLastEvent) => {
// Only fire the webhook if the last event was not passed.
const lastEvent = await getLastEvent()
const lastDecoded = lastEvent && decodeGovProposal(lastEvent.data)
if (
lastEvent &&
// If could not decode and verify that last event was passed, ignore to
// avoid spamming when something breaks.
(!lastDecoded ||
// If last event was passed (or passed + execution failed), ignore
// because already sent.
lastDecoded.status === ProposalStatus.PROPOSAL_STATUS_PASSED ||
lastDecoded.status === ProposalStatus.PROPOSAL_STATUS_FAILED)
) {
return
}

const { proposal, title, status } = decodeGovProposal(event.data)

return {
chainId: state.chainId,
type: 'proposal_executed',
data: {
chainId: state.chainId,
dao: getConfiguredChainConfig(state.chainId)?.name || 'GOV_PLACEHOLDER',
daoName: getDisplayNameForChainId(state.chainId),
imageUrl: getImageUrlForChainId(state.chainId),
proposalId: event.proposalId,
proposalTitle: proposal ? title : event.proposalId,
fromApprover: false,
failed: status === ProposalStatus.PROPOSAL_STATUS_FAILED,
},
}
},
})

// Fire webhook when a gov proposal is rejected.
export const makeInboxGovProposalRejected: WebhookMaker<GovStateEvent> = (
config,
state
) => ({
filter: {
EventType: GovStateEvent,
},
endpoint: {
type: WebhookType.Url,
url: 'https://notifier.daodao.zone/notify',
method: 'POST',
headers: {
'x-api-key': config.notifierSecret,
},
},
getValue: async (event, getLastEvent) => {
// Only fire the webhook if the last event was not rejected.
const lastEvent = await getLastEvent()
const lastDecoded = lastEvent && decodeGovProposal(lastEvent.data)
if (
lastEvent &&
// If could not decode and verify that last event was rejected, ignore to
// avoid spamming when something breaks.
(!lastDecoded ||
// If last event was rejected, ignore because already sent.
lastDecoded.status === ProposalStatus.PROPOSAL_STATUS_REJECTED)
) {
return
}

const { proposal, title } = decodeGovProposal(event.data)

return {
chainId: state.chainId,
type: 'proposal_closed',
data: {
chainId: state.chainId,
dao: getConfiguredChainConfig(state.chainId)?.name || 'GOV_PLACEHOLDER',
daoName: getDisplayNameForChainId(state.chainId),
imageUrl: getImageUrlForChainId(state.chainId),
proposalId: event.proposalId,
proposalTitle: proposal ? title : event.proposalId,
fromApprover: false,
},
}
},
})
44 changes: 41 additions & 3 deletions src/data/webhooks/websockets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getConfiguredChainConfig } from '@dao-dao/utils'

import { Webhook, WebhookMaker, WebhookType } from '@/core/types'
import { dbKeyForKeys, dbKeyToKeys } from '@/core/utils'
import { State, WasmStateEvent } from '@/db'
import { dbKeyForKeys, dbKeyToKeys, decodeGovProposal } from '@/core/utils'
import { GovStateEvent, State, WasmStateEvent } from '@/db'

import { activeProposalModules } from '../formulas/contract/daoCore/base'
import { getDaoAddressForProposalModule } from './utils'
Expand Down Expand Up @@ -85,7 +87,7 @@ export const makeBroadcastVoteCast: WebhookMaker<WasmStateEvent> = (
}

// Broadcast to WebSockets when a proposal status changes, including creation.
export const makeProposalStatusChanged: WebhookMaker<WasmStateEvent> = (
export const makeDaoProposalStatusChanged: WebhookMaker<WasmStateEvent> = (
config,
state
) =>
Expand Down Expand Up @@ -139,3 +141,39 @@ export const makeProposalStatusChanged: WebhookMaker<WasmStateEvent> = (
}
},
}

// Broadcast to WebSockets when a gov proposal status changes, including
// creation.
export const makeGovProposalStatusChanged: WebhookMaker<GovStateEvent> = (
config,
state
) =>
config.soketi && {
filter: {
EventType: GovStateEvent,
},
endpoint: {
type: WebhookType.Soketi,
channel: `${state.chainId}_${
getConfiguredChainConfig(state.chainId)?.name || 'GOV_PLACEHOLDER'
}`,
event: 'broadcast',
},
getValue: async (event, getLastEvent) => {
// Only fire the webhook when the status changes.
const lastEvent = await getLastEvent()
const lastStatus = lastEvent && decodeGovProposal(lastEvent.data)?.status
const status = decodeGovProposal(event.data)?.status
if (lastStatus && lastStatus === status) {
return
}

return {
type: 'proposal',
data: {
proposalId: event.proposalId,
status,
},
}
},
}

0 comments on commit 2d8cb51

Please sign in to comment.