Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions apps/dispatcher/src/services/triggers/base-trigger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AnticaptureClient } from '@notification-system/anticapture-client';
* @template T - Type of event data being processed
*/
export abstract class BaseTriggerHandler<T = any> implements TriggerHandler<T> {
private daoChainCache: Map<string, number> = new Map();
private daoCache: Map<string, { chainId: number; alreadySupportCalldataReview: boolean }> = new Map();

/**
* Creates a new instance of the BaseTriggerHandler
Expand Down Expand Up @@ -102,26 +102,34 @@ export abstract class BaseTriggerHandler<T = any> implements TriggerHandler<T> {
* @returns Chain ID for the DAO, or 1 (Ethereum mainnet) as default
* @throws Error if anticaptureClient is not provided
*/
protected async getChainIdForDao(daoId: string): Promise<number> {
/**
* Gets full DAO info (chainId, alreadySupportCalldataReview) with caching
*/
protected async getDaoInfo(daoId: string): Promise<{ chainId: number; alreadySupportCalldataReview: boolean }> {
if (!this.anticaptureClient) {
throw new Error('AnticaptureClient is required for getChainIdForDao');
throw new Error('AnticaptureClient is required for getDaoInfo');
}

// Check cache first
if (this.daoChainCache.has(daoId)) {
return this.daoChainCache.get(daoId)!;
if (this.daoCache.has(daoId)) {
return this.daoCache.get(daoId)!;
}

// Fetch DAOs and cache chain IDs
const daos = await this.anticaptureClient.getDAOs();
const daoMap = new Map(daos.map(dao => [dao.id, dao.chainId]));
for (const dao of daos) {
this.daoCache.set(dao.id, {
chainId: dao.chainId,
alreadySupportCalldataReview: dao.alreadySupportCalldataReview
});
}

// Cache all DAOs
daoMap.forEach((chainId, id) => {
this.daoChainCache.set(id, chainId);
});
return this.daoCache.get(daoId) || { chainId: 1, alreadySupportCalldataReview: false };
}

// Return chain ID for requested DAO or default to Ethereum mainnet
return daoMap.get(daoId) || 1;
/**
* Gets the chain ID for a specific DAO, with caching
*/
protected async getChainIdForDao(daoId: string): Promise<number> {
const info = await this.getDaoInfo(daoId);
return info.chainId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ describe('NewProposalTriggerHandler', () => {

mockAnticaptureClient = {
getDAOs: jest.fn(async () => [
{ id: 'dao123', chainId: 1 },
{ id: 'dao456', chainId: 10 }
{ id: 'dao123', chainId: 1, alreadySupportCalldataReview: false },
{ id: 'dao456', chainId: 10, alreadySupportCalldataReview: true }
]),
getProposalById: jest.fn(),
listProposals: jest.fn(),
Expand Down Expand Up @@ -150,6 +150,38 @@ describe('NewProposalTriggerHandler', () => {
expect(mockNotificationClient.sendNotification).not.toHaveBeenCalled();
});

it('should include calldata review button when DAO does not support it natively', async () => {
const mockMessage: DispatcherMessage = {
triggerId: 'new-proposal',
events: [{ ...mockProposal, daoId: 'dao123' }]
};

await handler.handleMessage(mockMessage);

const call = mockNotificationClient.sendNotification.mock.calls[0][0];
const buttons = call.metadata?.buttons;
expect(buttons).toBeDefined();
expect(buttons.some((b: any) => b.text.includes('Request a call-data review'))).toBe(true);
});

it('should NOT include calldata review button when DAO already supports it', async () => {
const mockMessage: DispatcherMessage = {
triggerId: 'new-proposal',
events: [{ ...mockProposal, id: 'prop789', daoId: 'dao456' }]
};

mockSubscriptionClient.shouldSend.mockResolvedValue([
{ user_id: '1', event_id: 'prop789', dao_id: 'dao456' }
]);

await handler.handleMessage(mockMessage);

const call = mockNotificationClient.sendNotification.mock.calls[0][0];
const buttons = call.metadata?.buttons;
expect(buttons).toBeDefined();
expect(buttons.some((b: any) => b.text.includes('Request a call-data review'))).toBe(false);
});

it('should extract title from multiline descriptions', async () => {
const mockUsersForMultiline: User[] = [
{ id: '1', channel: 'telegram', channel_user_id: '123', created_at: new Date() }
Expand Down Expand Up @@ -217,8 +249,8 @@ describe('NewProposalTriggerHandler - cross-DAO eventId deduplication', () => {
} as unknown as NotificationClientFactory,
{
getDAOs: async () => [
{ id: 'ens.eth', chainId: 1 },
{ id: 'uniswap.eth', chainId: 1 }
{ id: 'ens.eth', chainId: 1, alreadySupportCalldataReview: true },
{ id: 'uniswap.eth', chainId: 1, alreadySupportCalldataReview: false }
],
} as unknown as AnticaptureClient
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ export class NewProposalTriggerHandler extends BaseTriggerHandler {
title: proposalTitle
});

// Build buttons with transaction hash
const chainId = await this.getChainIdForDao(daoId);
// Build buttons with transaction hash and calldata review CTA
const daoInfo = await this.getDaoInfo(daoId);
const buttons = buildButtons({
triggerType: 'newProposal',
txHash: txHash,
chainId,
chainId: daoInfo.chainId,
daoId,
proposalId
proposalId,
alreadySupportCalldataReview: daoInfo.alreadySupportCalldataReview
});

await this.sendNotificationsToSubscribers(
Expand Down
1 change: 1 addition & 0 deletions packages/anticapture-client/dist/anticapture-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export declare class AnticaptureClient {
blockTime: number;
votingDelay: string;
chainId: number;
alreadySupportCalldataReview: boolean;
}>>;
/**
* Fetches a single proposal by ID with full type safety
Expand Down
3 changes: 2 additions & 1 deletion packages/anticapture-client/dist/anticapture-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ class AnticaptureClient {
// blockTime: dao.blockTime, // TODO: Uncomment when API supports this field
blockTime: 12, // Temporary hardcoded value - Ethereum block time
votingDelay: dao.votingDelay || '0',
chainId: dao.chainId
chainId: dao.chainId,
alreadySupportCalldataReview: dao.alreadySupportCalldataReview ?? false
}));
}
catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions packages/anticapture-client/dist/gql/gql.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n }\n }\n}": typeof types.GetDaOsDocument;
"query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n alreadySupportCalldataReview\n }\n }\n}": typeof types.GetDaOsDocument;
"query ListOffchainProposals($skip: NonNegativeInt, $limit: PositiveInt, $orderDirection: queryInput_offchainProposals_orderDirection, $status: JSON, $fromDate: Float) {\n offchainProposals(\n skip: $skip\n limit: $limit\n orderDirection: $orderDirection\n status: $status\n fromDate: $fromDate\n ) {\n items {\n id\n title\n discussion\n link\n state\n created\n }\n totalCount\n }\n}": typeof types.ListOffchainProposalsDocument;
"query ProposalNonVoters($id: String!, $addresses: JSON) {\n proposalNonVoters(id: $id, addresses: $addresses) {\n items {\n voter\n }\n }\n}": typeof types.ProposalNonVotersDocument;
"query GetProposalById($id: String!) {\n proposal(id: $id) {\n id\n daoId\n proposerAccountId\n title\n description\n startBlock\n endBlock\n endTimestamp\n timestamp\n status\n forVotes\n againstVotes\n abstainVotes\n txHash\n }\n}\n\nquery ListProposals($skip: NonNegativeInt, $limit: PositiveInt, $orderDirection: queryInput_proposals_orderDirection, $status: JSON, $fromDate: Float, $fromEndDate: Float, $includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals) {\n proposals(\n skip: $skip\n limit: $limit\n orderDirection: $orderDirection\n status: $status\n fromDate: $fromDate\n fromEndDate: $fromEndDate\n includeOptimisticProposals: $includeOptimisticProposals\n ) {\n items {\n id\n daoId\n proposerAccountId\n title\n description\n startBlock\n endBlock\n endTimestamp\n timestamp\n status\n forVotes\n againstVotes\n abstainVotes\n txHash\n }\n totalCount\n }\n}": typeof types.GetProposalByIdDocument;
Expand All @@ -37,7 +37,7 @@ export declare function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export declare function graphql(source: "query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n }\n }\n}"): (typeof documents)["query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n }\n }\n}"];
export declare function graphql(source: "query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n alreadySupportCalldataReview\n }\n }\n}"): (typeof documents)["query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n alreadySupportCalldataReview\n }\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/anticapture-client/dist/gql/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ exports.graphql = graphql;
/* eslint-disable */
const types = __importStar(require("./graphql"));
const documents = {
"query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n }\n }\n}": types.GetDaOsDocument,
"query GetDAOs {\n daos {\n items {\n id\n votingDelay\n chainId\n alreadySupportCalldataReview\n }\n }\n}": types.GetDaOsDocument,
"query ListOffchainProposals($skip: NonNegativeInt, $limit: PositiveInt, $orderDirection: queryInput_offchainProposals_orderDirection, $status: JSON, $fromDate: Float) {\n offchainProposals(\n skip: $skip\n limit: $limit\n orderDirection: $orderDirection\n status: $status\n fromDate: $fromDate\n ) {\n items {\n id\n title\n discussion\n link\n state\n created\n }\n totalCount\n }\n}": types.ListOffchainProposalsDocument,
"query ProposalNonVoters($id: String!, $addresses: JSON) {\n proposalNonVoters(id: $id, addresses: $addresses) {\n items {\n voter\n }\n }\n}": types.ProposalNonVotersDocument,
"query GetProposalById($id: String!) {\n proposal(id: $id) {\n id\n daoId\n proposerAccountId\n title\n description\n startBlock\n endBlock\n endTimestamp\n timestamp\n status\n forVotes\n againstVotes\n abstainVotes\n txHash\n }\n}\n\nquery ListProposals($skip: NonNegativeInt, $limit: PositiveInt, $orderDirection: queryInput_proposals_orderDirection, $status: JSON, $fromDate: Float, $fromEndDate: Float, $includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals) {\n proposals(\n skip: $skip\n limit: $limit\n orderDirection: $orderDirection\n status: $status\n fromDate: $fromDate\n fromEndDate: $fromEndDate\n includeOptimisticProposals: $includeOptimisticProposals\n ) {\n items {\n id\n daoId\n proposerAccountId\n title\n description\n startBlock\n endBlock\n endTimestamp\n timestamp\n status\n forVotes\n againstVotes\n abstainVotes\n txHash\n }\n totalCount\n }\n}": types.GetProposalByIdDocument,
Expand Down
2 changes: 2 additions & 0 deletions packages/anticapture-client/dist/gql/graphql.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ export type CompareVotes_200_Response = {
};
export type Dao_200_Response = {
__typename?: 'dao_200_response';
alreadySupportCalldataReview: Scalars['Boolean']['output'];
chainId: Scalars['Float']['output'];
id: Scalars['String']['output'];
proposalThreshold: Scalars['String']['output'];
Expand Down Expand Up @@ -1681,6 +1682,7 @@ export type GetDaOsQuery = {
id: string;
votingDelay: string;
chainId: number;
alreadySupportCalldataReview: boolean;
}>;
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/anticapture-client/dist/gql/graphql.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading