Skip to content
4 changes: 2 additions & 2 deletions apps/consumers/src/services/dao/slack-dao.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const TEST_API_URL = 'http://test-api';

const anticaptureClient = makeAnticaptureClient({
getDAOs: async () => [
{ id: 'UNI', chainId: 1, blockTime: 12, votingDelay: '0', alreadySupportCalldataReview: false, supportOffchainData: false },
{ id: 'ENS', chainId: 1, blockTime: 12, votingDelay: '0', alreadySupportCalldataReview: false, supportOffchainData: false },
{ id: 'UNI', chainId: 1, blockTime: 12, votingDelay: '0', supportsCalldataReview: false, supportsOffchainData: false },
{ id: 'ENS', chainId: 1, blockTime: 12, votingDelay: '0', supportsCalldataReview: false, supportsOffchainData: false },
],
});

Expand Down
12 changes: 6 additions & 6 deletions apps/dispatcher/src/services/triggers/base-trigger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { createLogger, type Logger } from '@anticapture/observability';
* @template T - Type of event data being processed
*/
export abstract class BaseTriggerHandler<T = any> implements TriggerHandler<T> {
private daoCache: Map<string, { chainId: number; alreadySupportCalldataReview: boolean; supportOffchainData: boolean }> = new Map();
private daoCache: Map<string, { chainId: number; supportsCalldataReview: boolean; supportsOffchainData: boolean }> = new Map();

protected readonly logger: Logger;

Expand Down Expand Up @@ -112,9 +112,9 @@ export abstract class BaseTriggerHandler<T = any> implements TriggerHandler<T> {
* @throws Error if anticaptureClient is not provided
*/
/**
* Gets full DAO info (chainId, alreadySupportCalldataReview) with caching
* Gets full DAO info (chainId, supportsCalldataReview) with caching
*/
protected async getDaoInfo(daoId: string): Promise<{ chainId: number; alreadySupportCalldataReview: boolean; supportOffchainData: boolean }> {
protected async getDaoInfo(daoId: string): Promise<{ chainId: number; supportsCalldataReview: boolean; supportsOffchainData: boolean }> {
if (!this.anticaptureClient) {
throw new Error('AnticaptureClient is required for getDaoInfo');
}
Expand All @@ -127,12 +127,12 @@ export abstract class BaseTriggerHandler<T = any> implements TriggerHandler<T> {
for (const dao of daos) {
this.daoCache.set(dao.id, {
chainId: dao.chainId,
alreadySupportCalldataReview: dao.alreadySupportCalldataReview,
supportOffchainData: dao.supportOffchainData
supportsCalldataReview: dao.supportsCalldataReview,
supportsOffchainData: dao.supportsOffchainData
});
}

return this.daoCache.get(daoId) || { chainId: 1, alreadySupportCalldataReview: false, supportOffchainData: false };
return this.daoCache.get(daoId) || { chainId: 1, supportsCalldataReview: false, supportsOffchainData: false };
}

/**
Expand Down
4 changes: 2 additions & 2 deletions apps/dispatcher/src/services/triggers/helpers/test-doubles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export function makeDao(overrides: Partial<DaoInfo> = {}): DaoInfo {
blockTime: 12,
votingDelay: '1',
chainId: 1,
alreadySupportCalldataReview: false,
supportOffchainData: false,
supportsCalldataReview: false,
supportsOffchainData: false,
...overrides,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ describe('NewProposalTriggerHandler', () => {

const anticaptureClient = makeAnticaptureClient({
getDAOs: async () => [
makeDao({ id: 'dao123', alreadySupportCalldataReview: false }),
makeDao({ id: 'dao456', chainId: 10, alreadySupportCalldataReview: true, supportOffchainData: true }),
makeDao({ id: 'dao123', supportsCalldataReview: false }),
makeDao({ id: 'dao456', chainId: 10, supportsCalldataReview: true, supportsOffchainData: true }),
],
});

Expand Down Expand Up @@ -183,8 +183,8 @@ describe('NewProposalTriggerHandler - cross-DAO eventId deduplication', () => {
},
makeAnticaptureClient({
getDAOs: async () => [
{ id: 'ens.eth', blockTime: 12, votingDelay: '1', chainId: 1, alreadySupportCalldataReview: true, supportOffchainData: true },
{ id: 'uniswap.eth', blockTime: 12, votingDelay: '1', chainId: 1, alreadySupportCalldataReview: false, supportOffchainData: true }
{ id: 'ens.eth', blockTime: 12, votingDelay: '1', chainId: 1, supportsCalldataReview: true, supportsOffchainData: true },
{ id: 'uniswap.eth', blockTime: 12, votingDelay: '1', chainId: 1, supportsCalldataReview: false, supportsOffchainData: true }
],
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class NewProposalTriggerHandler extends BaseTriggerHandler {
chainId: daoInfo.chainId,
daoId,
proposalId,
alreadySupportCalldataReview: daoInfo.alreadySupportCalldataReview
alreadySupportCalldataReview: daoInfo.supportsCalldataReview
});

await this.sendNotificationsToSubscribers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ describe('VoteConfirmationTriggerHandler', () => {
await handler.handleMessage({
triggerId: NotificationTypeId.VoteConfirmation,
events: [
{ daoId: 'test-dao', proposalId: 'proposal-1', voterAddress: '0xVoter123', support: '1', votingPower: '1000000000000000000', timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 1' },
{ daoId: 'test-dao', proposalId: 'proposal-2', voterAddress: '0xVoter123', support: '0', votingPower: '1000000000000000000', timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 2' },
{ daoId: 'test-dao', proposalId: 'proposal-3', voterAddress: '0xVoter123', support: '2', votingPower: '1000000000000000000', timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 3' },
{ daoId: 'test-dao', proposalId: 'proposal-1', voterAddress: '0xVoter123', support: '1', votingPower: 1000000000000000000n, timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 1' },
{ daoId: 'test-dao', proposalId: 'proposal-2', voterAddress: '0xVoter123', support: '0', votingPower: 1000000000000000000n, timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 2' },
{ daoId: 'test-dao', proposalId: 'proposal-3', voterAddress: '0xVoter123', support: '2', votingPower: 1000000000000000000n, timestamp: 1767225600, transactionHash: '0xSameTxHash', proposalTitle: 'Proposal 3' },
]
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class VoteConfirmationTriggerHandler extends BaseTriggerHandler<VoteWithD

private formatVoteMessage(vote: VoteWithDaoId): string {
const supportKey = voteConfirmationMessages.getSupportKey(String(vote.support));
const votingPower = formatTokenAmount(vote.votingPower, 18);
const votingPower = formatTokenAmount(String(vote.votingPower), 18);
const hasReason = vote.reason && vote.reason.trim();

const messageTemplate = hasReason
Expand Down
2 changes: 1 addition & 1 deletion apps/integrated-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "vitest run"
},
"devDependencies": {
"@anticapture/client": "^1.1.0",
"@anticapture/client": "^1.2.0",
"@notification-system/anticapture-client": "workspace:*",
"@notification-system/consumer": "workspace:*",
"@notification-system/dispatcher": "workspace:*",
Expand Down
10 changes: 5 additions & 5 deletions apps/integrated-tests/src/setup/msw-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
offchainProposalNonVotersHandler,
votesByProposalIdHandler,
votesOffchainByProposalIdHandler,
getDaosHandler
daosHandler
} from '@anticapture/client/msw';
import { testConstants } from '../config';

Expand Down Expand Up @@ -78,8 +78,8 @@ type WithDao = { daoId: string } | { spaceId: string };
const idOf = (p: WithDao) => ('daoId' in p ? p.daoId : p.spaceId);
export const daosFromItems = (items: ReadonlyArray<WithDao>) => {
const ids = Array.from(new Set(items.map(idOf)));
return getDaosHandler({
items: ids.map(id => ({ id, votingDelay: '0', supportOffchainData: true })),
return daosHandler({
items: ids.map(id => ({ id, votingDelay: '0', supportsOffchainData: true })),
totalCount: ids.length,
});
};
Expand All @@ -90,11 +90,11 @@ const emptyThreshold = { threshold: 0 };
const testDaos = Object.values(testConstants.daoIds).map(id => ({
id,
votingDelay: '0',
supportOffchainData: true
supportsOffchainData: true
}));

export const defaultHandlers = [
getDaosHandler({ items: testDaos, totalCount: testDaos.length }),
daosHandler({ items: testDaos, totalCount: testDaos.length }),
offchainProposalsHandler(emptyListEnvelope),
votesOffchainHandler(emptyListEnvelope),
offchainProposalNonVotersHandler(emptyListEnvelope),
Expand Down
4 changes: 2 additions & 2 deletions apps/integrated-tests/tests/core/duplicate-prevention.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, beforeAll, afterEach } from 'vitest';
import { proposalsHandler, getDaosHandler, offchainProposalsHandler } from '@anticapture/client/msw';
import { proposalsHandler, daosHandler, offchainProposalsHandler } from '@anticapture/client/msw';
import { HttpResponse } from 'msw';
import type { OnchainProposal } from '@notification-system/anticapture-client';
import { db, TestApps } from '../../src/setup';
Expand All @@ -10,7 +10,7 @@ import { testConstants, timeouts } from '../../src/config';

const useDaoAndActiveProposal = (daoId: string, proposal: OnchainProposal) =>
server.use(
getDaosHandler({ items: [{ id: daoId, votingDelay: '0' }], totalCount: 1 }),
daosHandler({ items: [{ id: daoId, votingDelay: '0' }], totalCount: 1 }),
offchainProposalsHandler({ items: [], totalCount: 0 }),
proposalsHandler(({ request }) => {
const statuses = new URL(request.url).searchParams.getAll('status').map(s => s.toLowerCase());
Expand Down
6 changes: 3 additions & 3 deletions apps/logic-system/src/triggers/proposal-finished-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export class ProposalFinishedTrigger extends Trigger<ProposalOnChain, void> {
description: proposal?.description || '',
endTimestamp: proposal?.endTimestamp ?? 0,
status: proposal?.status || 'unknown',
forVotes: proposal?.forVotes || '0',
againstVotes: proposal?.againstVotes || '0',
abstainVotes: proposal?.abstainVotes || '0'
forVotes: proposal?.forVotes != null ? String(proposal.forVotes) : '0',
againstVotes: proposal?.againstVotes != null ? String(proposal.againstVotes) : '0',
abstainVotes: proposal?.abstainVotes != null ? String(proposal.abstainVotes) : '0'
}));

// Send all proposals in a single batch message for maximum efficiency
Expand Down
14 changes: 14 additions & 0 deletions apps/subscription-server/src/services/settings.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ describe('SettingsService', () => {

expect(result).toEqual([]);
});

test('drops rows whose trigger_type is no longer in the current enum', async () => {
stub.findByUserResult = [
{ user_id: 'user-1', trigger_type: NotificationTypeId.NewProposal, is_active: true, updated_at: FIXED_DATE },
{ user_id: 'user-1', trigger_type: 'offchain-voting-reminder-75' as NotificationTypeId, is_active: false, updated_at: FIXED_DATE },
{ user_id: 'user-1', trigger_type: 'totally-made-up' as NotificationTypeId, is_active: true, updated_at: FIXED_DATE },
];

const result = await settingsService.getUserPreferences('user-1');

expect(result).toEqual([
{ user_id: 'user-1', trigger_type: NotificationTypeId.NewProposal, is_active: true, updated_at: FIXED_DATE },
]);
});
});

describe('saveUserPreferences', () => {
Expand Down
6 changes: 4 additions & 2 deletions apps/subscription-server/src/services/settings.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { NotificationTypeId } from '@notification-system/messages';
import { NotificationTypeId } from '@notification-system/messages';
import { IUserNotificationPreferencesRepository, UserNotificationPreference } from '../interfaces/user_subscription.interface';

export class SettingsService {
constructor(private prefsRepo: IUserNotificationPreferencesRepository) {}

async getUserPreferences(userId: string): Promise<UserNotificationPreference[]> {
return this.prefsRepo.findByUser(userId);
const stored = await this.prefsRepo.findByUser(userId);
const validTriggerTypes = new Set<string>(Object.values(NotificationTypeId));
return stored.filter(p => validTriggerTypes.has(p.trigger_type));
}

async saveUserPreferences(
Expand Down
2 changes: 1 addition & 1 deletion packages/anticapture-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"vitest": "^4.1.5"
},
"dependencies": {
"@anticapture/client": "^1.1.0",
"@anticapture/client": "^1.2.0",
"viem": "^2.34.0"
}
}
23 changes: 13 additions & 10 deletions packages/anticapture-client/src/anticapture-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
getDaos,
daos,
proposal,
proposals,
getEventRelevanceThreshold,
Expand Down Expand Up @@ -54,6 +54,9 @@ function processVotingPowerHistory(
.map(item => ({
...item,
daoId,
timestamp: String(item.timestamp),
votingPower: String(item.votingPower),
delta: String(item.delta),
changeType: item.delegation ? 'delegation' : item.transfer ? 'transfer' : 'other',
sourceAccountId: item.transfer?.from || item.delegation?.from || '',
targetAccountId: item.accountId,
Expand All @@ -73,7 +76,7 @@ export interface AnticaptureClientConfig {
export type VoteWithDaoId = OnchainVote & { daoId: string };
export type OffchainVoteWithDaoId = OffchainVote & { daoId: string };

export type DaoInfo = { id: string; blockTime: number; votingDelay: string; chainId: number; alreadySupportCalldataReview: boolean; supportOffchainData: boolean };
export type DaoInfo = { id: string; blockTime: number; votingDelay: string; chainId: number; supportsCalldataReview: boolean; supportsOffchainData: boolean };

/**
* Public surface of AnticaptureClient — used for dependency injection and mocking.
Expand Down Expand Up @@ -129,17 +132,17 @@ export class AnticaptureClient implements IAnticaptureClient {
private toChecksum<T>(o: T): T { return this.normalizeAddressesInObject(o, getAddress); }
private toLowercase<T>(o: T): T { return this.normalizeAddressesInObject(o, a => a.toLowerCase()); }

async getDAOs(): Promise<Array<{ id: string; blockTime: number; votingDelay: string; chainId: number; alreadySupportCalldataReview: boolean; supportOffchainData: boolean }>> {
async getDAOs(): Promise<Array<DaoInfo>> {
try {
const res = await this.call(() => getDaos(this.sdkConfig));
const res = await this.call(() => daos(this.sdkConfig));
const items = res.items ?? [];
return items.map(d => ({
id: d.id,
blockTime: 12,
votingDelay: d.votingDelay ?? '0',
chainId: d.chainId ?? 1,
alreadySupportCalldataReview: d.alreadySupportCalldataReview ?? false,
supportOffchainData: d.supportOffchainData ?? false,
supportsCalldataReview: d.supportsCalldataReview ?? false,
supportsOffchainData: d.supportsOffchainData ?? false,
}));
} catch (err) {
console.warn('Returning empty DAO list due to API error:', err instanceof Error ? err.message : err);
Expand All @@ -152,7 +155,7 @@ export class AnticaptureClient implements IAnticaptureClient {
for (const dao of allDaos) {
try {
// SDK dao param is a string-literal enum; DAO IDs come from runtime /daos response, so we cast
const res = await this.call(() => proposal(dao.id as any, id, this.sdkConfig));
const res = await this.call(() => proposal(dao.id as any, id, undefined, this.sdkConfig));
if (res) return this.toLowercase(res);
} catch (err) {
const e = err as { status?: number; response?: { status?: number } };
Expand Down Expand Up @@ -280,7 +283,7 @@ export class AnticaptureClient implements IAnticaptureClient {
try {
// SDK dao param is a string-literal enum; DAO IDs come from runtime /daos response, so we cast
const res = await this.call(() => getEventRelevanceThreshold(daoId as any, { type, relevance }, this.sdkConfig));
return res?.threshold ?? null;
return res?.threshold != null ? String(res.threshold) : null;
} catch (err) {
console.warn(`[AnticaptureClient] Error fetching threshold for ${daoId}/${type}:`, err instanceof Error ? err.message : err);
return null;
Expand All @@ -302,7 +305,7 @@ export class AnticaptureClient implements IAnticaptureClient {
const allDaos = await this.getDAOs();
const all: (OffchainProposalItem & { daoId: string })[] = [];
for (const dao of allDaos) {
if (!dao.supportOffchainData) continue;
if (!dao.supportsOffchainData) continue;
try {
const res = await this.call(() => offchainProposals(dao.id as any, this.toChecksum(variables ?? {}), this.sdkConfig));
const items = (res?.items ?? []).map(i => ({ ...i, daoId: dao.id }));
Expand Down Expand Up @@ -330,7 +333,7 @@ export class AnticaptureClient implements IAnticaptureClient {
const daos = await this.getDAOs();
const voteArrays = await Promise.all(
daos
.filter(dao => dao.supportOffchainData)
.filter(dao => dao.supportsOffchainData)
.map(async (dao) => {
const vs = await this.listOffchainVotes(dao.id, {
fromDate,
Expand Down
5 changes: 4 additions & 1 deletion packages/anticapture-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { HistoricalVotingPower, OffchainProposal, OffchainVote } from '@ant
export type OffchainProposalItem = OffchainProposal;
export type OffchainVoteItem = OffchainVote;

export type ProcessedVotingPowerHistory = HistoricalVotingPower & {
export type ProcessedVotingPowerHistory = Omit<HistoricalVotingPower, 'timestamp' | 'votingPower' | 'delta'> & {
timestamp: string;
votingPower: string;
delta: string;
changeType: 'delegation' | 'transfer' | 'other';
sourceAccountId: string;
targetAccountId: string;
Expand Down
Loading
Loading