Skip to content

Commit fd6ba16

Browse files
authored
Event-based Metrics (#147)
This PR adds a number of metrics for errors in parsing/tx handling -- especially for our squads monitoring. It also removes the old once-per-epoch flush in favor of real time events sent to influxdb. NOTE: This means that any dashboard currently reliant on the per-epoch data will break. Though I assume this is fine, as we weren't measuring much to begin with.
1 parent 2f2bd11 commit fd6ba16

17 files changed

Lines changed: 723 additions & 148 deletions

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
RUST_LOG=info
2-
ENDPOINT=
2+
YELLOWSTONE_URL=
3+
RPC_URL=
34
X_TOKEN=
45
FAILED=true
56
ACCOUNT_INCLUDE=SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy,Vau1t6sLNxnzB7ZDsef8TLbPLfyZMYXH8WTNqUdm9g8

docker-compose.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ services:
88
environment:
99
- RUST_LOG=${RUST_LOG:-debug}
1010
- YELLOWSTONE_URL=${YELLOWSTONE_URL}
11+
- RPC_URL=${RPC_URL}
1112
- X_TOKEN=${X_TOKEN}
1213
- ACCOUNT_INCLUDE=${ACCOUNT_INCLUDE}
1314
- CONFIG_FILE=${CONFIG_FILE}
1415
- SOLANA_METRICS_CONFIG=${SOLANA_METRICS_CONFIG}
1516
- SLACK_WEBHOOK_URL={SLACK_WEBHOOK_URL}
1617
- STAKE_POOL_ALERTS_SLACK_WEBHOOK_URL={STAKE_POOL_ALERTS_SLACK_WEBHOOK_URL}
18+
- SQUADS_ALERTS_SLACK_WEBHOOK_URL={SQUADS_ALERTS_SLACK_WEBHOOK_URL}
1719
- DISCORD_WEBHOOK_URL={DISCORD_WEBHOOK_URL}
1820
- TELEGRAM_BOT_TOKEN={TELEGRAM_BOT_TOKEN}
1921
- TELEGRAM_CHAT_ID={TELEGRAM_CHAT_ID}

jito-bell/src/bin/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async fn main() -> anyhow::Result<()> {
7171

7272
let commitment = CommitmentConfig::confirmed();
7373
let mut handler = JitoBellHandler::new(
74-
args.yellowstone_url.clone(),
74+
args.rpc_url.clone(),
7575
commitment,
7676
args.config_file,
7777
subscribe_option,

jito-bell/src/cli_args.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ pub struct Args {
1010
/// Yellowstone gRPC endpoint URL
1111
pub yellowstone_url: String,
1212

13+
#[clap(long, env = "RPC_URL")]
14+
/// Solana JSON-RPC endpoint URL
15+
pub rpc_url: String,
16+
1317
#[clap(long, env = "X_TOKEN")]
1418
pub x_token: Option<String>,
1519

@@ -53,6 +57,10 @@ pub struct Args {
5357
#[clap(long, env)]
5458
pub stakenet_event_alerts_slack_webhook_url: Option<String>,
5559

60+
/// Slack webhook URL for Squads Alerts
61+
#[clap(long, env)]
62+
pub squads_alerts_slack_webhook_url: Option<String>,
63+
5664
/// Discord webhook URL
5765
#[clap(long, env)]
5866
pub discord_webhook_url: Option<String>,

jito-bell/src/config.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,10 @@ pub struct JitoBellConfig {
1212
/// Block explorer url
1313
pub explorer_url: String,
1414

15-
/// Squads app URL template
16-
#[serde(default = "default_squads_app_url_template")]
17-
pub squads_app_url_template: String,
18-
1915
/// Message Templates
2016
pub message_templates: HashMap<String, String>,
2117
}
2218

23-
fn default_squads_app_url_template() -> String {
24-
"https://app.squads.so/squads/{{multisig}}/transactions/{{transaction}}".to_string()
25-
}
26-
2719
impl JitoBellConfig {
2820
/// Get a message template by name, falling back to default
2921
pub fn get_template(&self, name: &str) -> Option<&String> {
@@ -52,11 +44,6 @@ mod tests {
5244
impl std::fmt::Display for JitoBellConfig {
5345
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5446
writeln!(f, "Explorer URL: {}", self.explorer_url)?;
55-
writeln!(
56-
f,
57-
"Squads App URL Template: {}",
58-
self.squads_app_url_template
59-
)?;
6047

6148
writeln!(f, "Message Templates:")?;
6249
for (name, template) in &self.message_templates {

jito-bell/src/handlers/mod.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod stake_pool;
1313
mod vault;
1414

1515
use borsh::BorshDeserialize;
16-
use log::debug;
16+
use log::{debug, warn};
1717
use solana_rpc_client::nonblocking::rpc_client::RpcClient;
1818
use solana_sdk::{program_pack::Pack, pubkey::Pubkey};
1919
use spl_token::state::Mint;
@@ -123,23 +123,37 @@ pub(crate) async fn send_notification(
123123
}
124124
}
125125
InstructionParser::SquadsV3(ix) => match ix {
126-
SquadsV3Program::CreateTransaction { .. } => {
126+
SquadsV3Program::CreateTransaction { multisig, .. } => {
127+
handler.increment_squads_parsed();
127128
if let Some(instruction) =
128129
handler.get_instruction_config(ProgramName::SquadsV3, ix)
129130
{
130131
squads_v3::handle_squads_v3_program(handler, parser, ix, &instruction)
131132
.await?;
133+
} else {
134+
handler.increment_squads_no_config();
135+
warn!(
136+
"SquadsV3: CreateTransaction from multisig {} has no handler config",
137+
multisig
138+
);
132139
}
133140
}
134141
SquadsV3Program::ActivateTransaction { ix: _ } => {}
135142
},
136143
InstructionParser::SquadsV4(ix) => match ix {
137-
SquadsV4Program::ProposalCreate { .. } => {
144+
SquadsV4Program::ProposalCreate { multisig, .. } => {
145+
handler.increment_squads_parsed();
138146
if let Some(instruction) =
139147
handler.get_instruction_config(ProgramName::SquadsV4, ix)
140148
{
141149
squads_v4::handle_squads_v4_program(handler, parser, ix, &instruction)
142150
.await?;
151+
} else {
152+
handler.increment_squads_no_config();
153+
warn!(
154+
"SquadsV4: ProposalCreate from multisig {} has no handler config",
155+
multisig
156+
);
143157
}
144158
}
145159
SquadsV4Program::ProposalActivate { ix: _ } => {}

jito-bell/src/handlers/squads_common.rs

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
//! Shared types for Squads v3/v4 notification handling.
22
3-
use solana_sdk::pubkey::Pubkey;
3+
use base64::{engine::general_purpose::STANDARD, Engine as _};
4+
use solana_sdk::{pubkey, pubkey::Pubkey};
5+
6+
const SQUADS_V4_URL: &str =
7+
"https://app.squads.so/squads/{{multisig}}/transactions/{{transaction}}";
8+
const SQUADS_V4_PROGRAM_ID: Pubkey = pubkey!("SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf");
9+
const SQUADS_V4_SEED_PREFIX: &[u8] = b"multisig";
10+
const SQUADS_V4_SEED_VAULT: &[u8] = b"vault";
11+
const SQUADS_V4_SEED_TRANSACTION: &[u8] = b"transaction";
412

513
#[derive(Clone, Copy, Debug)]
6-
pub(crate) enum SquadsContext {
14+
pub enum SquadsContext {
715
V3Transaction {
816
multisig: Pubkey,
917
transaction: Pubkey,
@@ -52,10 +60,22 @@ impl SquadsContext {
5260

5361
fn transaction_template_value(self) -> String {
5462
match self {
55-
Self::V3Transaction { transaction, .. } => transaction.to_string(),
63+
Self::V3Transaction { transaction, .. } => STANDARD.encode(transaction.to_string()),
5664
Self::V4Proposal {
57-
transaction_index, ..
58-
} => transaction_index.to_string(),
65+
multisig,
66+
transaction_index,
67+
..
68+
} => Pubkey::find_program_address(
69+
&[
70+
SQUADS_V4_SEED_PREFIX,
71+
multisig.as_ref(),
72+
SQUADS_V4_SEED_TRANSACTION,
73+
&transaction_index.to_le_bytes(),
74+
],
75+
&SQUADS_V4_PROGRAM_ID,
76+
)
77+
.0
78+
.to_string(),
5979
}
6080
}
6181

@@ -75,10 +95,99 @@ impl SquadsContext {
7595
}
7696
}
7797

78-
pub(crate) fn squads_url(self, template: &str) -> String {
79-
template
80-
.replace("{{multisig}}", &self.multisig().to_string())
81-
.replace("{{transaction}}", &self.transaction_template_value())
82-
.replace("{{proposal}}", &self.proposal_template_value())
98+
fn multisig_template_value(self) -> String {
99+
match self {
100+
// V3 URL has no {{multisig}} placeholder; value unused but kept consistent.
101+
Self::V3Transaction { .. } => self.multisig().to_string(),
102+
// The v4 app route uses the squad's vault PDA, not the multisig account.
103+
Self::V4Proposal { multisig, .. } => Pubkey::find_program_address(
104+
&[
105+
SQUADS_V4_SEED_PREFIX,
106+
multisig.as_ref(),
107+
SQUADS_V4_SEED_VAULT,
108+
&[0],
109+
],
110+
&SQUADS_V4_PROGRAM_ID,
111+
)
112+
.0
113+
.to_string(),
114+
}
115+
}
116+
117+
pub(crate) fn squads_url(self, _explorer_url: &str, _transaction_signature: &str) -> String {
118+
match self {
119+
Self::V3Transaction {
120+
multisig,
121+
transaction,
122+
} => format!(
123+
"https://v3.squads.so/transactions/{}/tx/{}",
124+
STANDARD.encode(multisig.to_string()),
125+
transaction,
126+
),
127+
Self::V4Proposal { .. } => SQUADS_V4_URL
128+
.replace("{{multisig}}", &self.multisig_template_value())
129+
.replace("{{transaction}}", &self.transaction_template_value())
130+
.replace("{{proposal}}", &self.proposal_template_value()),
131+
}
132+
}
133+
134+
pub fn build_slack_payload(
135+
self,
136+
description: &str,
137+
transaction_signature: &str,
138+
explorer_url: &str,
139+
) -> serde_json::Value {
140+
let squads_url = self.squads_url(explorer_url, transaction_signature);
141+
let mut fields = vec![
142+
serde_json::json!({
143+
"type": "mrkdwn",
144+
"text": format!("*Squads:* <{}|{}>", squads_url, self.link_label())
145+
}),
146+
serde_json::json!({
147+
"type": "mrkdwn",
148+
"text": format!(
149+
"*Transaction:* <{}/tx/{}|View on Explorer>",
150+
explorer_url, transaction_signature
151+
)
152+
}),
153+
serde_json::json!({
154+
"type": "mrkdwn",
155+
"text": format!("*Multisig:* `{}`", self.multisig())
156+
}),
157+
serde_json::json!({
158+
"type": "mrkdwn",
159+
"text": format!("*{}:* `{}`", self.account_label(), self.account())
160+
}),
161+
];
162+
163+
if let Some(transaction_index) = self.transaction_index_field() {
164+
fields.push(serde_json::json!({
165+
"type": "mrkdwn",
166+
"text": format!("*Transaction Index:* `{}`", transaction_index)
167+
}));
168+
}
169+
170+
serde_json::json!({
171+
"blocks": [
172+
{
173+
"type": "header",
174+
"text": {
175+
"type": "plain_text",
176+
"text": self.header()
177+
}
178+
},
179+
{
180+
"type": "section",
181+
"text": {
182+
"type": "mrkdwn",
183+
"text": format!("*Description:* {}", description)
184+
}
185+
},
186+
{
187+
"type": "section",
188+
"fields": fields
189+
}
190+
]
191+
})
83192
}
84193
}

jito-bell/src/ix_parser/squads_v3.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use log::debug;
12
use solana_sdk::{
23
hash::hash,
34
instruction::{AccountMeta, Instruction},
@@ -39,15 +40,26 @@ impl SquadsV3Program {
3940
pub fn parse_squads_v3_program<T: ParsableInstruction>(
4041
instruction: &T,
4142
account_keys: &[Pubkey],
43+
squads_parse_errors: &mut u64,
4244
) -> Option<SquadsV3Program> {
4345
let discriminator: [u8; 8] = instruction.data().get(..8)?.try_into().ok()?;
4446

4547
if discriminator == anchor_discriminator(Self::CREATE_TRANSACTION) {
46-
return Self::parse_create_transaction_ix(instruction, account_keys);
48+
let result = Self::parse_create_transaction_ix(instruction, account_keys);
49+
if result.is_none() {
50+
*squads_parse_errors += 1;
51+
debug!("SquadsV3: matched create_transaction discriminator but failed to parse (short data or invalid account index)");
52+
}
53+
return result;
4754
}
4855

4956
if discriminator == anchor_discriminator(Self::ACTIVATE_TRANSACTION) {
50-
return Self::parse_activate_transaction_ix(instruction, account_keys);
57+
let result = Self::parse_activate_transaction_ix(instruction, account_keys);
58+
if result.is_none() {
59+
*squads_parse_errors += 1;
60+
debug!("SquadsV3: matched activate_transaction discriminator but failed to parse (short data or invalid account index)");
61+
}
62+
return result;
5163
}
5264

5365
None
@@ -130,7 +142,7 @@ mod tests {
130142
data.extend_from_slice(&[1, 2, 3, 4]);
131143
let instruction = create_compiled_instruction(4, vec![0, 1, 2, 3], data.clone());
132144

133-
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys);
145+
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys, &mut 0);
134146

135147
let Some(SquadsV3Program::CreateTransaction {
136148
multisig,
@@ -150,7 +162,7 @@ mod tests {
150162
data.extend_from_slice(&[1, 2, 3, 4]);
151163
let instruction = create_compiled_instruction(3, vec![0, 1, 2], data.clone());
152164

153-
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys);
165+
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys, &mut 0);
154166

155167
let Some(SquadsV3Program::ActivateTransaction { ix }) = parsed else {
156168
panic!("Expected ActivateTransaction variant");
@@ -167,12 +179,31 @@ mod tests {
167179
);
168180
}
169181

182+
#[test]
183+
fn test_create_transaction_with_missing_accounts_increments_error_counter() {
184+
let account_keys = create_test_pubkeys(2);
185+
let mut data = anchor_discriminator("create_transaction").to_vec();
186+
data.extend_from_slice(&[1, 2, 3, 4]);
187+
// No accounts provided — parse_create_transaction_ix will return None
188+
let instruction = create_compiled_instruction(1, vec![], data);
189+
190+
let mut errors = 0u64;
191+
let parsed =
192+
SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys, &mut errors);
193+
194+
assert!(parsed.is_none());
195+
assert_eq!(
196+
errors, 1,
197+
"known discriminator with missing accounts should increment parse error counter"
198+
);
199+
}
200+
170201
#[test]
171202
fn test_unknown_data_returns_none() {
172203
let account_keys = create_test_pubkeys(3);
173204
let instruction = create_compiled_instruction(0, vec![0, 1, 2], vec![0; 8]);
174205

175-
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys);
206+
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys, &mut 0);
176207

177208
assert!(parsed.is_none());
178209
}
@@ -182,7 +213,7 @@ mod tests {
182213
let account_keys = create_test_pubkeys(3);
183214
let instruction = create_compiled_instruction(0, vec![0, 1, 2], vec![0; 7]);
184215

185-
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys);
216+
let parsed = SquadsV3Program::parse_squads_v3_program(&instruction, &account_keys, &mut 0);
186217

187218
assert!(parsed.is_none());
188219
}

0 commit comments

Comments
 (0)