Skip to content

Commit fe99035

Browse files
Merge pull request #214 from blockful/refactor/votes_on_chain_route
Refactor/votes on chain route
2 parents 3e10138 + 8846e81 commit fe99035

28 files changed

Lines changed: 1666 additions & 1035 deletions

apps/dispatcher/src/services/triggers/new-proposal-trigger.service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('NewProposalTriggerHandler', () => {
8080
getProposalById: jest.fn(),
8181
listProposals: jest.fn(),
8282
listVotingPowerHistory: jest.fn(),
83-
listVotesOnchains: jest.fn(),
83+
listVotes: jest.fn(),
8484
listRecentVotesFromAllDaos: jest.fn()
8585
} as unknown as jest.Mocked<AnticaptureClient>;
8686

apps/dispatcher/src/services/triggers/non-voting-handler.test-factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export function createUser(overrides: Partial<User> = {}): User {
5353
};
5454
}
5555

56-
export function createVote(voterAccountId: string, proposalId: string) {
56+
export function createVote(voterAddress: string, proposalId: string) {
5757
return {
58-
voterAccountId,
58+
voterAddress,
5959
proposalId
6060
};
6161
}

apps/dispatcher/src/services/triggers/vote-confirmation-trigger.service.ts

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,13 @@ import { BaseTriggerHandler } from './base-trigger.service';
22
import { DispatcherMessage, MessageProcessingResult } from '../../interfaces/dispatcher-message.interface';
33
import { NotificationClientFactory } from '../notification/notification-factory.service';
44
import { ISubscriptionClient } from '../../interfaces/subscription-client.interface';
5-
import { AnticaptureClient } from '@notification-system/anticapture-client';
5+
import { AnticaptureClient, VoteWithDaoId } from '@notification-system/anticapture-client';
66
import { formatTokenAmount } from '../../lib/number-formatter';
77
import { voteConfirmationMessages, replacePlaceholders, buildButtons } from '@notification-system/messages';
8-
import { FormattingService } from '../formatting.service';
9-
10-
interface VoteEvent {
11-
daoId: string;
12-
proposalId: string;
13-
voterAccountId: string;
14-
support: string;
15-
votingPower: string;
16-
timestamp: string;
17-
txHash: string;
18-
reason?: string | null;
19-
proposalDescription?: string | null;
20-
}
218

229
interface UserVoteCombination {
2310
user: any;
24-
vote: VoteEvent;
11+
vote: VoteWithDaoId;
2512
}
2613

2714
interface ProcessingResult {
@@ -33,7 +20,7 @@ interface ProcessingResult {
3320
type ProcessingStatus = 'sent' | 'skipped';
3421

3522

36-
export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent> {
23+
export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteWithDaoId> {
3724
constructor(
3825
protected readonly subscriptionClient: ISubscriptionClient,
3926
protected readonly notificationFactory: NotificationClientFactory,
@@ -42,7 +29,7 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
4229
super(subscriptionClient, notificationFactory, anticaptureClient);
4330
}
4431

45-
async handleMessage(message: DispatcherMessage<VoteEvent>): Promise<MessageProcessingResult> {
32+
async handleMessage(message: DispatcherMessage<VoteWithDaoId>): Promise<MessageProcessingResult> {
4633
const events = message.events;
4734

4835
if (!events || events.length === 0) {
@@ -54,7 +41,7 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
5441
}
5542

5643
// Extract unique voter addresses and batch fetch wallet owners
57-
const voterAddresses = [...new Set(events.map(event => event.voterAccountId))];
44+
const voterAddresses = [...new Set(events.map(event => event.voterAddress))];
5845
const walletOwners = await this.subscriptionClient.getWalletOwnersBatch(voterAddresses);
5946

6047
// Create all user-vote combinations
@@ -75,11 +62,11 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
7562
* Creates combinations of users and votes for processing
7663
*/
7764
private createUserVoteCombinations(
78-
events: VoteEvent[],
65+
events: VoteWithDaoId[],
7966
walletOwners: Record<string, any[]>
8067
): UserVoteCombination[] {
8168
return events.flatMap(voteEvent => {
82-
const usersForWallet = walletOwners[voteEvent.voterAccountId] || [];
69+
const usersForWallet = walletOwners[voteEvent.voterAddress] || [];
8370
return usersForWallet.map(user => ({ user, vote: voteEvent }));
8471
});
8572
}
@@ -106,9 +93,9 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
10693
/**
10794
* Processes a single user-vote combination
10895
*/
109-
private async processUserVote(user: any, vote: VoteEvent): Promise<ProcessingStatus> {
96+
private async processUserVote(user: any, vote: VoteWithDaoId): Promise<ProcessingStatus> {
11097
// Check if user is subscribed to the DAO
111-
const subscribers = await this.getSubscribers(vote.daoId, vote.txHash, vote.timestamp);
98+
const subscribers = await this.getSubscribers(vote.daoId, vote.transactionHash, String(vote.timestamp));
11299
const isSubscribed = subscribers.some(sub => sub.id === user.id);
113100

114101
if (!isSubscribed) {
@@ -117,9 +104,9 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
117104
}
118105

119106
// Check deduplication
120-
const notifications = await this.subscriptionClient.shouldSend([user], vote.txHash, vote.daoId);
107+
const notifications = await this.subscriptionClient.shouldSend([user], vote.transactionHash, vote.daoId);
121108
if (notifications.length === 0) {
122-
console.log(`[VoteConfirmationHandler] Notification already sent for vote ${vote.txHash}`);
109+
console.log(`[VoteConfirmationHandler] Notification already sent for vote ${vote.transactionHash}`);
123110
return 'skipped';
124111
}
125112

@@ -133,55 +120,51 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteEvent
133120
/**
134121
* Sends notification for a single vote
135122
*/
136-
private async sendVoteNotification(user: any, vote: VoteEvent): Promise<void> {
123+
private async sendVoteNotification(user: any, vote: VoteWithDaoId): Promise<void> {
137124
const message = this.formatVoteMessage(vote);
138125
const chainId = await this.getChainIdForDao(vote.daoId);
139126

140127
// Build buttons
141128
const buttons = buildButtons({
142129
triggerType: 'voteConfirmation',
143-
txHash: vote.txHash,
130+
txHash: vote.transactionHash,
144131
chainId
145132
});
146133

147134
await this.sendNotificationsToSubscribers(
148135
[user],
149136
message,
150-
vote.txHash,
137+
vote.transactionHash,
151138
vote.daoId,
152139
{
153140
transaction: {
154-
hash: vote.txHash,
141+
hash: vote.transactionHash,
155142
chainId
156143
},
157144
addresses: {
158-
address: vote.voterAccountId
145+
address: vote.voterAddress
159146
},
160147
proposalId: vote.proposalId,
161-
support: vote.support,
148+
support: String(vote.support),
162149
votingPower: vote.votingPower,
163150
reason: vote.reason
164151
},
165152
buttons
166153
);
167154
}
168155

169-
private formatVoteMessage(vote: VoteEvent): string {
170-
const supportKey = voteConfirmationMessages.getSupportKey(vote.support);
156+
private formatVoteMessage(vote: VoteWithDaoId): string {
157+
const supportKey = voteConfirmationMessages.getSupportKey(String(vote.support));
171158
const votingPower = formatTokenAmount(vote.votingPower, 18);
172159
const hasReason = vote.reason && vote.reason.trim();
173-
const proposalTitle = FormattingService.extractTitle(
174-
vote.proposalDescription || '',
175-
vote.proposalId
176-
);
177160

178161
const messageTemplate = hasReason
179162
? voteConfirmationMessages.withReason[supportKey]
180163
: voteConfirmationMessages.withoutReason[supportKey];
181164

182165
return replacePlaceholders(messageTemplate, {
183166
daoId: vote.daoId,
184-
proposalTitle,
167+
proposalTitle: vote.proposalTitle,
185168
votingPower,
186169
...(hasReason && { reason: vote.reason! })
187170
});
Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { v4 as uuidv4 } from 'uuid';
22

33
export interface VoteData {
4-
id: string;
54
proposalId: string;
6-
voterAccountId: string;
5+
voterAddress: string;
76
daoId: string;
8-
support: string; // Must be string for Zod validation in AntiCapture client
9-
weight: string;
10-
reason?: string;
11-
timestamp: string;
12-
txHash: string;
7+
support: number;
8+
reason?: string | null;
9+
timestamp: number;
10+
transactionHash: string;
1311
votingPower: string;
12+
proposalTitle: string;
1413
}
1514

1615
/**
@@ -19,63 +18,63 @@ export interface VoteData {
1918
export class VoteFactory {
2019
/**
2120
* Creates a vote for testing
22-
* @param voterAccountId The address of the voter
21+
* @param voterAddress The address of the voter
2322
* @param proposalId The proposal being voted on
2423
* @param daoId The DAO identifier
2524
* @param overrides Optional field overrides
2625
* @returns Vote data object
2726
*/
2827
static createVote(
29-
voterAccountId: string,
28+
voterAddress: string,
3029
proposalId: string,
3130
daoId: string,
3231
overrides: Partial<VoteData> = {}
3332
): VoteData {
3433
return {
35-
id: uuidv4(),
3634
proposalId,
37-
voterAccountId,
35+
voterAddress,
3836
daoId,
39-
support: '1', // '1' = FOR, '0' = AGAINST, '2' = ABSTAIN (must be string for Zod validation)
40-
weight: '1000000000000000000', // 1 token in wei
41-
timestamp: new Date().toISOString(),
42-
txHash: `0x${uuidv4().replace(/-/g, '')}${uuidv4().replace(/-/g, '').substring(0, 8)}`, // Generate a fake tx hash
43-
votingPower: '1000000000000000000', // 1 token in wei
37+
support: 1,
38+
timestamp: Math.floor(Date.now() / 1000),
39+
transactionHash: `0x${uuidv4().replace(/-/g, '')}${uuidv4().replace(/-/g, '').substring(0, 8)}`,
40+
votingPower: '1000000000000000000',
41+
proposalTitle: 'Test Proposal',
42+
reason: null,
4443
...overrides
4544
};
4645
}
4746

4847
/**
4948
* Creates multiple votes for different proposals
50-
* @param voterAccountId The address of the voter
49+
* @param voterAddress The address of the voter
5150
* @param proposalIds Array of proposal IDs to vote on
5251
* @param daoId The DAO identifier
5352
* @returns Array of vote data objects
5453
*/
5554
static createVotesForProposals(
56-
voterAccountId: string,
55+
voterAddress: string,
5756
proposalIds: string[],
5857
daoId: string
5958
): VoteData[] {
60-
return proposalIds.map(proposalId =>
61-
this.createVote(voterAccountId, proposalId, daoId)
59+
return proposalIds.map(proposalId =>
60+
this.createVote(voterAddress, proposalId, daoId)
6261
);
6362
}
6463

6564
/**
6665
* Creates votes from multiple voters for a single proposal
67-
* @param voterAccountIds Array of voter addresses
66+
* @param voterAddresses Array of voter addresses
6867
* @param proposalId The proposal being voted on
6968
* @param daoId The DAO identifier
7069
* @returns Array of vote data objects
7170
*/
7271
static createVotesFromMultipleVoters(
73-
voterAccountIds: string[],
72+
voterAddresses: string[],
7473
proposalId: string,
7574
daoId: string
7675
): VoteData[] {
77-
return voterAccountIds.map(voterAccountId =>
78-
this.createVote(voterAccountId, proposalId, daoId)
76+
return voterAddresses.map(voterAddress =>
77+
this.createVote(voterAddress, proposalId, daoId)
7978
);
8079
}
8180
}

apps/integrated-tests/src/mocks/graphql-mock-setup.ts

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,46 +89,52 @@ export class GraphQLMockSetup {
8989
}
9090

9191
// Handle votes
92-
if (data.query?.includes('ListVotesOnchains')) {
92+
if (data.query?.includes('ListVotes')) {
9393
let filtered = votesData;
94-
95-
const daoId = data.variables?.daoId;
96-
const proposalIdIn = data.variables?.proposalId_in;
97-
const voterAccountIdIn = data.variables?.voterAccountId_in;
98-
const timestampGt = data.variables?.timestamp_gt;
99-
100-
// Filter by daoId if provided
94+
95+
const daoId = config?.headers?.['anticapture-dao-id'];
96+
const voterAddressIn = data.variables?.voterAddressIn;
97+
const fromDate = data.variables?.fromDate;
98+
const toDate = data.variables?.toDate;
99+
100+
// Filter by daoId
101101
if (daoId) {
102102
filtered = filtered.filter((v: any) => v.daoId === daoId);
103103
}
104-
105-
// Filter by proposalId_in if provided
106-
if (proposalIdIn) {
107-
filtered = filtered.filter((v: any) =>
108-
proposalIdIn.includes(v.proposalId)
109-
);
110-
}
111-
112-
// Filter by voterAccountId_in if provided
113-
// Using case-insensitive comparison (simulates API's internal normalization)
114-
if (voterAccountIdIn) {
104+
105+
// Filter by voterAddressIn if provided
106+
if (voterAddressIn && Array.isArray(voterAddressIn)) {
115107
filtered = filtered.filter((v: any) =>
116-
voterAccountIdIn.some((addr: string) =>
117-
isAddressEqual(getAddress(v.voterAccountId), getAddress(addr))
108+
voterAddressIn.some((addr: string) =>
109+
isAddressEqual(getAddress(v.voterAddress), getAddress(addr))
118110
)
119111
);
120112
}
121113

122-
// Filter by timestamp_gt if provided
123-
if (timestampGt) {
124-
filtered = filtered.filter((v: any) =>
125-
parseInt(v.timestamp || '0') > parseInt(timestampGt)
126-
);
114+
// Filter by fromDate if provided
115+
if (fromDate !== undefined) {
116+
filtered = filtered.filter((v: any) => v.timestamp > fromDate);
127117
}
128118

129-
// Return votes in original format (checksum) - AnticaptureClient will normalize to lowercase
119+
// Filter by toDate if provided
120+
if (toDate !== undefined) {
121+
filtered = filtered.filter((v: any) => v.timestamp < toDate);
122+
}
123+
124+
// Return items in expected format
125+
const items = filtered.map((v: any) => ({
126+
transactionHash: v.transactionHash,
127+
proposalId: v.proposalId,
128+
voterAddress: v.voterAddress,
129+
support: v.support,
130+
votingPower: v.votingPower,
131+
timestamp: v.timestamp,
132+
reason: v.reason || null,
133+
proposalTitle: v.proposalTitle
134+
}));
135+
130136
return Promise.resolve({
131-
data: { data: { votesOnchains: { items: filtered, totalCount: filtered.length } } }
137+
data: { data: { votes: { items, totalCount: items.length } } }
132138
});
133139
}
134140

@@ -141,7 +147,7 @@ export class GraphQLMockSetup {
141147
const votersSet = new Set(
142148
votesData
143149
.filter((v: any) => v.proposalId === proposalId)
144-
.map((v: any) => getAddress(v.voterAccountId).toLowerCase())
150+
.map((v: any) => getAddress(v.voterAddress).toLowerCase())
145151
);
146152

147153
// Filter to find non-voters from the provided address list
@@ -179,7 +185,7 @@ export class GraphQLMockSetup {
179185
proposals: { items: [], totalCount: 0 },
180186
proposal: null,
181187
daos: { items: [] },
182-
votesOnchains: { items: [] }
188+
votes: { items: [], totalCount: 0 }
183189
}
184190
}
185191
});

0 commit comments

Comments
 (0)