Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rule evaluation errors are rate-limited: only the first occurrence and every 100th recurrence are logged (#60).
- `SourceAccount`'s `allow`/`deny` fields are deprecated in favour of `mode` + `accounts`; both previously fired on list *membership*, which reversed the expected "alert on unknown caller" behaviour (#250).
- `NoActivity` incidents now carry a unique `alert_id` per incident, and the recovery alert reuses its incident's `alert_id` so the two can be correlated (#254, #255).
- `alert_id` now hashes the network cursor ID, the rule type, the rule label and the rule's index within its contract's `rules` list — previously it only hashed `contract_id`, `tx_hash` and `rule_triggered`, so two rules with identical labels on one contract (e.g. two `LargeTransfer(>=100XLM)` rules routed to different webhooks) produced the same ID and were deduplicated away by receivers (#252). **IDs change**: receivers that persist `alert_id` for deduplication will see new values for alerts generated after upgrading, so old entries can no longer be matched against new ones. `EvalContext` gains a `network_id` field and `check_no_activity` gains a `rule_index` parameter.

### Removed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ service. See [Multiple destinations](docs/configuration.md#multiple-destinations

**Fields:**
- `schema_version` — integer version of this payload shape (currently `1`). Additive changes (new optional fields) keep the same version; breaking changes (field removals or renames) bump it. Receivers should use this to detect incompatible changes.
- `alert_id` — stable, deterministic identifier derived from `(network, contract_id, tx_hash, rule_type, rule_triggered)` via SHA-256 prefix (32 hex chars). Identical for every retry of the same alert. Receivers should deduplicate on this value.
- `alert_id` — stable, deterministic identifier derived from `(network, contract_id, tx_hash, rule_type, rule_triggered, rule_index)` via SHA-256 prefix (32 hex chars), where `network` is the network cursor ID (e.g. `custom@https://…` for custom networks) and `rule_index` is the rule's position in the contract's `rules` list. Identical for every retry of the same alert, but never shared between two rules — even two rules with identical labels get different IDs (#252). Receivers should deduplicate on this value.
- `rule_type` — stable machine-readable rule variant (e.g. `"LargeTransfer"`, `"HighFee"`); use this for programmatic routing
- `rule_triggered` — human-readable rule description with parameters (e.g. `"LargeTransfer(>=10000XLM)"`); use this for display
- `function_name` — the first invoked Soroban function name, or `null` for non-Soroban transactions.
Expand Down
3 changes: 3 additions & 0 deletions crates/notifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,15 @@ pub fn test_payload_with_network(
schema_version: 1,
// Unique per send, so repeated tests are not de-duplicated by receivers.
alert_id: txwatch_rules::alert_id(
network,
TEST_CONTRACT_ID,
tx_hash,
rule_type,
&format!(
"TestWebhook@{}",
now.timestamp_nanos_opt().unwrap_or_default()
),
0,
),
label: label.to_string(),
contract_id: TEST_CONTRACT_ID.into(),
Expand Down
3 changes: 3 additions & 0 deletions crates/poller/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@ async fn poll_contract<S: TransactionSource + ?Sized>(
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: canonical_base,
explorer_base: contract.network.explorer_base_url(),
};
Expand Down Expand Up @@ -1779,6 +1780,7 @@ async fn poll_contract<S: TransactionSource + ?Sized>(
}
if let Some(payload) = check_no_activity(
&rule.rule,
idx,
state.last_seen,
now,
&mut state.no_activity_states[idx],
Expand Down Expand Up @@ -2061,6 +2063,7 @@ fn evaluate_contract(
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base,
explorer_base: contract.network.explorer_base_url(),
};
Expand Down
8 changes: 8 additions & 0 deletions crates/poller/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ async fn any_transaction_fires_webhook() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: &horizon.uri(),
explorer_base: Some("https://stellar.expert/explorer/testnet"),
};
Expand Down Expand Up @@ -377,6 +378,7 @@ async fn transaction_failed_rule_fires_only_on_failure() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: &horizon.uri(),
explorer_base: Some("https://stellar.expert/explorer/testnet"),
};
Expand Down Expand Up @@ -439,6 +441,7 @@ async fn large_transfer_fires_above_threshold() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
},
Expand Down Expand Up @@ -527,6 +530,7 @@ async fn function_called_rule_fires_on_exact_match() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
},
Expand Down Expand Up @@ -737,6 +741,7 @@ async fn high_fee_rule_fires_on_fee_charged() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: &horizon.uri(),
explorer_base: Some("https://stellar.expert/explorer/testnet"),
},
Expand Down Expand Up @@ -1757,6 +1762,7 @@ async fn large_transfer_only_counts_transfers_involving_the_contract() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
};
Expand Down Expand Up @@ -1834,6 +1840,7 @@ async fn large_transfer_respects_direction_option() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
};
Expand Down Expand Up @@ -1907,6 +1914,7 @@ async fn contract_upgraded_rule_fires_with_wasm_hashes() {
label: &contract.label,
contract_id: &contract.contract_id,
network: contract.network.as_str(),
network_id: contract.network.cursor_id(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
},
Expand Down
1 change: 1 addition & 0 deletions crates/rules/benches/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fn bench_evaluate(c: &mut Criterion) {
label: "bench",
contract_id: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4",
network: "testnet",
network_id: "testnet".into(),
horizon_base: "https://horizon-testnet.stellar.org",
explorer_base: Some("https://stellar.expert/explorer/testnet"),
};
Expand Down
Loading