Skip to content
Merged

Dev #229

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3e4b0e7
feat: generated files
LeonardoVieira1630 Feb 26, 2026
2901dd2
feat: massages for the snapshot proposal case
LeonardoVieira1630 Feb 26, 2026
eb39c32
feat: update anticapture client to query offchain proposals
LeonardoVieira1630 Feb 26, 2026
35d6978
feat: listOffchainProposals function
LeonardoVieira1630 Feb 26, 2026
99c30fa
feat: NewOffchainProposalTrigger instance on app.ts
LeonardoVieira1630 Feb 26, 2026
5c28466
feat: offchain proposal interface
LeonardoVieira1630 Feb 26, 2026
aa72541
feat: repository to call the client for offchain proposals
LeonardoVieira1630 Feb 26, 2026
b772f3c
feat: new trigger on logic-system
LeonardoVieira1630 Feb 26, 2026
ce32195
add: new trigger handler on dispatcher
LeonardoVieira1630 Feb 26, 2026
ddfd801
feat: new handler instance on dispatcher
LeonardoVieira1630 Feb 26, 2026
21aee2b
feat: MSW lib
LeonardoVieira1630 Feb 26, 2026
850e889
feat: unit tests
LeonardoVieira1630 Feb 26, 2026
2bd2b97
feat: factory to offchain proposals
LeonardoVieira1630 Feb 26, 2026
e37ffe8
feat: offchain proposal event support on createMockImplementation
LeonardoVieira1630 Feb 26, 2026
91351a0
feat: imports
LeonardoVieira1630 Feb 26, 2026
0ef24e7
feat: integration test
LeonardoVieira1630 Feb 26, 2026
391e3e5
feat: add Bearer token auth for AntiCapture API Gateway
alextnetto Mar 3, 2026
75db667
feat: generated files
LeonardoVieira1630 Mar 4, 2026
203e29f
feat: gql files
LeonardoVieira1630 Mar 4, 2026
6521cb3
feat: GetEventRelevanceThreshold query
LeonardoVieira1630 Mar 4, 2026
c4feda8
add: threshold repo instance on app.ts
LeonardoVieira1630 Mar 4, 2026
4e565ea
feat: threshold repository
LeonardoVieira1630 Mar 4, 2026
1fd2f75
fix: typesafety on dispatcher
LeonardoVieira1630 Mar 4, 2026
9446dd1
refactor: vp trigger to filter by threshold
LeonardoVieira1630 Mar 4, 2026
755e132
feat: logic system's tests
LeonardoVieira1630 Mar 4, 2026
f4813b3
feat: type exports and tests on the anticapture client
LeonardoVieira1630 Mar 4, 2026
0c37eef
refactor: metadata on each trigger
LeonardoVieira1630 Mar 4, 2026
4fb82a8
feat: utm functions
LeonardoVieira1630 Mar 4, 2026
f625e46
feat: utm addition on url
LeonardoVieira1630 Mar 4, 2026
5acca05
refactor: unit tests
LeonardoVieira1630 Mar 4, 2026
5f0260a
Merge pull request #225 from blockful/feat/anticapture-api-auth-token
pikonha Mar 5, 2026
f702037
refactor: update common UI messages for onboarding flow
LeonardoVieira1630 Mar 5, 2026
e8c9b13
feat: Telegram linear onboarding flow with select/unselect all
LeonardoVieira1630 Mar 5, 2026
272f7ce
refactor: update Slack UI messages to match Telegram copy
LeonardoVieira1630 Mar 5, 2026
f934b1f
feat: Slack linear onboarding flow with select/unselect all
LeonardoVieira1630 Mar 5, 2026
a075cf6
add: onboarding flow to slack
LeonardoVieira1630 Mar 5, 2026
9dd9cba
refactor: remove select all buttons from slack
LeonardoVieira1630 Mar 5, 2026
795e71b
refactor: code improviments
LeonardoVieira1630 Mar 5, 2026
141ab12
Merge pull request #227 from blockful/refactor/CTAs_and_utm_tracking
LeonardoVieira1630 Mar 5, 2026
cce5d1c
Merge branch 'dev' into feat/new_offchain_proposal_notification
LeonardoVieira1630 Mar 6, 2026
2b29530
feat: proposal link to be added on the button
LeonardoVieira1630 Mar 6, 2026
df41da0
Merge pull request #224 from blockful/feat/new_offchain_proposal_noti…
LeonardoVieira1630 Mar 6, 2026
3c0c0ed
Merge pull request #228 from blockful/refactor/onboarding_flow
LeonardoVieira1630 Mar 6, 2026
cbba9a2
Merge branch 'dev' into feat/relevance_threshold_route
LeonardoVieira1630 Mar 6, 2026
d993cf1
add: generated files
LeonardoVieira1630 Mar 6, 2026
e9c83d0
add: link to the offchain tests
LeonardoVieira1630 Mar 6, 2026
4b163a1
add: link on mock
LeonardoVieira1630 Mar 6, 2026
7a080ed
Merge pull request #226 from blockful/feat/relevance_threshold_route
LeonardoVieira1630 Mar 9, 2026
cc5a1d2
add: RPC_URL as an optional value for consumer
LeonardoVieira1630 Mar 9, 2026
bf5dd44
Merge pull request #230 from blockful/refactor/ENS_resolver_performance
LeonardoVieira1630 Mar 9, 2026
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# === REQUIRED CONFIGURATION ===
ANTICAPTURE_GRAPHQL_ENDPOINT=https://api-gateway-production-0879.up.railway.app/graphql
BLOCKFUL_API_TOKEN=

# === NOTIFICATION PLATFORMS ===
TELEGRAM_BOT_TOKEN=
Expand Down
55 changes: 43 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,39 +181,68 @@ ALTER TABLE votes_onchain ENABLE TRIGGER ALL;
```

### Voting Power Change Insert (Delegation Received)

**Important:** The underlying tables are in a UUID schema (e.g. `ecbe454c-b8fe-4659-8864-4cc68148cfde`), not in `anticapture` (which has views). Find the correct schema via: `SELECT pg_get_viewdef('anticapture.voting_power_history', true);`

**Critical:** Use a **fixed transaction hash** (not `extract(epoch from now())`) so both tables share the exact same value. The API joins `voting_power_history` with `delegations` on `transaction_hash`, and requires `delegation.log_index < voting_power_history.log_index`. If the join fails, `changeType` becomes `'other'`, which bypasses threshold filtering entirely.

```sql
SET search_path TO "<schema_uuid>";
SET search_path TO "<underlying_uuid_schema>";
ALTER TABLE voting_power_history DISABLE TRIGGER ALL;
ALTER TABLE delegations DISABLE TRIGGER ALL;

INSERT INTO voting_power_history (transaction_hash, dao_id, account_id, voting_power, delta, delta_mod, timestamp, log_index)
-- Insert delegations FIRST with log_index = 0
INSERT INTO delegations (transaction_hash, dao_id, delegate_account_id, delegator_account_id, delegated_value, previous_delegate, timestamp, log_index)
VALUES (
'0xmock_vp_' || extract(epoch from now())::bigint,
'0xmock_vp_test',
'ENS',
'<user_wallet_address>',
5000000000000000000,
1000000000000000000,
'0x1111111111111111111111111111111111111111',
1000000000000000000,
'0x0000000000000000000000000000000000000000',
extract(epoch from now())::bigint,
1
0 -- must be < voting_power_history.log_index
);

INSERT INTO delegations (transaction_hash, dao_id, delegate_account_id, delegator_account_id, delegated_value, previous_delegate, timestamp, log_index)
-- Then insert voting_power_history with log_index = 1
INSERT INTO voting_power_history (transaction_hash, dao_id, account_id, voting_power, delta, delta_mod, timestamp, log_index)
VALUES (
'0xmock_vp_' || extract(epoch from now())::bigint,
'0xmock_vp_test',
'ENS',
'<user_wallet_address>',
'0x1111111111111111111111111111111111111111',
5000000000000000000,
1000000000000000000,
1000000000000000000,
'0x0000000000000000000000000000000000000000',
extract(epoch from now())::bigint,
1
1 -- must be > delegation.log_index
);

ALTER TABLE voting_power_history ENABLE TRIGGER ALL;
ALTER TABLE delegations ENABLE TRIGGER ALL;
```

### New Offchain Proposal Insert (Snapshot)
```sql
-- No triggers on snapshot.proposals, safe to insert directly
INSERT INTO snapshot.proposals (id, space_id, author, title, body, discussion, type, start, "end", state, created, updated, link, flagged)
VALUES (
'0xmock_offchain_' || extract(epoch from now())::bigint,
'ens.eth',
'0x1111111111111111111111111111111111111111',
'[MOCK] Test Offchain Proposal for Notification System',
'This is a mock offchain proposal inserted for testing the notification pipeline.',
'<discussion_url_from_existing_proposal>', -- e.g. https://discuss.ens.domains/t/...
'single-choice',
extract(epoch from now())::integer,
(extract(epoch from now()) + 604800)::integer, -- ends in 7 days
'active',
extract(epoch from now())::integer,
extract(epoch from now())::integer,
'<link_from_existing_proposal>', -- copy from: SELECT link FROM snapshot.proposals ORDER BY created DESC LIMIT 5
false
);
```

### Cleanup Mock Data
```sql
SET search_path TO "<schema_uuid>";
Expand All @@ -224,6 +253,7 @@ ALTER TABLE delegations DISABLE TRIGGER ALL;
DELETE FROM votes_onchain WHERE tx_hash LIKE '0xmock%';
DELETE FROM voting_power_history WHERE transaction_hash LIKE '0xmock%';
DELETE FROM delegations WHERE transaction_hash LIKE '0xmock%';
DELETE FROM snapshot.proposals WHERE id LIKE '0xmock%';

ALTER TABLE votes_onchain ENABLE TRIGGER ALL;
ALTER TABLE voting_power_history ENABLE TRIGGER ALL;
Expand All @@ -241,5 +271,6 @@ SELECT v.* FROM votes_onchain v LEFT JOIN proposals_onchain p ON v.proposal_id =
-- List mock records
SELECT 'VOTE' as type, tx_hash, timestamp FROM votes_onchain WHERE tx_hash LIKE '0xmock%'
UNION ALL SELECT 'VP', transaction_hash, timestamp FROM voting_power_history WHERE transaction_hash LIKE '0xmock%'
UNION ALL SELECT 'DELEG', transaction_hash, timestamp FROM delegations WHERE transaction_hash LIKE '0xmock%';
UNION ALL SELECT 'DELEG', transaction_hash, timestamp FROM delegations WHERE transaction_hash LIKE '0xmock%'
UNION ALL SELECT 'OFFCHAIN', id, created FROM snapshot.proposals WHERE id LIKE '0xmock%';
```
1 change: 1 addition & 0 deletions apps/consumers/example.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
ANTICAPTURE_GRAPHQL_ENDPOINT=https://api-gateway/graphql
BLOCKFUL_API_TOKEN=
SUBSCRIPTION_SERVER_URL=http://localhost:3001
API_PORT=3000
4 changes: 4 additions & 0 deletions apps/consumers/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ const envSchema = z.object({
SLACK_SIGNING_SECRET: z.string(),
TOKEN_ENCRYPTION_KEY: z.string(),
ANTICAPTURE_GRAPHQL_ENDPOINT: z.string().url("ANTICAPTURE_GRAPHQL_ENDPOINT must be a valid URL"),
BLOCKFUL_API_TOKEN: z.string().optional(),
SUBSCRIPTION_SERVER_URL: z.string(),
RABBITMQ_URL: z.string().url(),
PORT: z.coerce.number().positive().optional().default(3002),
RPC_URL: z.string().optional(),
});

export function loadConfig() {
Expand All @@ -24,8 +26,10 @@ export function loadConfig() {
slackSigningSecret: env.SLACK_SIGNING_SECRET,
tokenEncryptionKey: env.TOKEN_ENCRYPTION_KEY,
anticaptureGraphqlEndpoint: env.ANTICAPTURE_GRAPHQL_ENDPOINT,
blockfulApiToken: env.BLOCKFUL_API_TOKEN,
subscriptionServerUrl: env.SUBSCRIPTION_SERVER_URL,
rabbitmqUrl: env.RABBITMQ_URL,
port: env.PORT,
rpcUrl: env.RPC_URL
} as const;
}
11 changes: 9 additions & 2 deletions apps/consumers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { SlackClient } from './clients/slack.client';
const config = loadConfig();

// Create ENS resolver
const ensResolver = new EnsResolverService();
const ensResolver = new EnsResolverService(config.rpcUrl);

// Create Telegram client for production
const telegramClient = new TelegramClient(config.telegramBotToken);
Expand All @@ -35,7 +35,14 @@ const slackClient = new SlackClient(
// Create and start the application
const app = new App(
config.subscriptionServerUrl,
axios.create({ baseURL: config.anticaptureGraphqlEndpoint }),
axios.create({
baseURL: config.anticaptureGraphqlEndpoint,
headers: {
...(config.blockfulApiToken && {
Authorization: `Bearer ${config.blockfulApiToken}`,
}),
},
}),
config.rabbitmqUrl,
ensResolver,
telegramClient,
Expand Down
1 change: 1 addition & 0 deletions apps/consumers/src/interfaces/bot.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ContextWithSession extends Context {
walletAction?: 'add' | 'remove';
walletsToRemove?: Set<string>;
awaitingWalletInput?: boolean;
fromStart?: boolean;
};
}

Expand Down
2 changes: 2 additions & 0 deletions apps/consumers/src/interfaces/slack-context.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface SlackBodyWithIds {
};
};
} | string; // Can be string for DialogSubmitAction or object for BlockAction
actions?: Array<{ action_id?: string; value?: string; type?: string }>;
}

/**
Expand All @@ -56,6 +57,7 @@ export interface SlackSession {
type: 'wallet' | 'dao';
action: 'add' | 'remove';
};
fromStart?: boolean;
}

/**
Expand Down
29 changes: 26 additions & 3 deletions apps/consumers/src/services/bot/slack-bot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { SlackClientInterface } from '../../interfaces/slack-client.interface';
import { NotificationPayload } from '../../interfaces/notification.interface';
import { BotServiceInterface } from '../../interfaces/bot-service.interface';
import { slackMessages, convertMarkdownToSlack } from '@notification-system/messages';
import { slackMessages, convertMarkdownToSlack, appendUtmParams } from '@notification-system/messages';
import { EnsResolverService } from '../ens-resolver.service';
import { SlackDAOService } from '../dao/slack-dao.service';
import { SlackWalletService } from '../wallet/slack-wallet.service';
Expand Down Expand Up @@ -48,6 +48,11 @@ export class SlackBotService implements BotServiceInterface {
// Welcome message actions
handlers.action('welcome_select_daos', async (ctx) => {
if (this.daoService) {
const channelId = ctx.body.channel?.id;
const workspaceId = ctx.body.team?.id || ctx.body.user?.team_id;
const fullUserId = `${workspaceId}:${channelId}`;
const hasDaos = await this.daoService.hasSubscriptions(fullUserId);
ctx.session.fromStart = !hasDaos;
await this.daoService.initialize(ctx);
}
});
Expand All @@ -74,13 +79,20 @@ export class SlackBotService implements BotServiceInterface {
handlers.action('dao_confirm_subscribe', async (ctx) => {
if (this.daoService) {
await this.daoService.confirm(ctx);

// If from onboarding flow, trigger wallet setup
if (ctx.session.fromStart && this.walletService) {
await this.walletService.showOnboardingWallet(ctx);
ctx.session.fromStart = false;
}
}
});

handlers.action('dao_checkboxes', async (ctx) => {
await ctx.ack();
});


handlers.action('wallet_checkboxes', async (ctx) => {
await ctx.ack();
});
Expand All @@ -94,6 +106,8 @@ export class SlackBotService implements BotServiceInterface {

handlers.action('wallet_add', async (ctx) => {
if (this.walletService) {
const firstAction = ctx.body.actions?.[0];
ctx.session.fromStart = firstAction?.value === 'onboarding';
await this.walletService.startAddWallet(ctx);
}
});
Expand Down Expand Up @@ -212,16 +226,25 @@ export class SlackBotService implements BotServiceInterface {
throw new Error('Slack notification requires workspace OAuth token. No bot_token provided in notification payload.');
}

// Append UTM tracking params to button URLs
const triggerType = payload.metadata?.triggerType;
const buttons = payload.metadata?.buttons?.map(btn => ({
text: btn.text,
url: triggerType
? appendUtmParams(btn.url, { source: 'notification', medium: 'slack', campaign: triggerType })
: btn.url
}));

// Build message options with buttons if provided
const messageOptions = payload.metadata?.buttons ? {
const messageOptions = buttons ? {
blocks: [
{
type: 'section' as const,
text: { type: 'mrkdwn' as const, text: processedMessage }
},
{
type: 'actions' as const,
elements: payload.metadata.buttons.map(btn => ({
elements: buttons.map(btn => ({
type: 'button' as const,
text: { type: 'plain_text' as const, text: btn.text },
url: btn.url
Expand Down
Loading