Skip to content
Open

Dev #233

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