serviceability: add Feed catalog account and CRUD#3953
Conversation
Add a Feed account (metro(exchange) -> group-set catalog) with create/update/delete managed by a catalog admin (FEED_AUTHORITY Permission or FOUNDATION), plus Rust SDK command builders and a 'doublezero feed' CLI. A feed with no metros imposes no restriction.
|
should we add |
| pub metros: Vec<(Pubkey, Vec<Pubkey>)>, | ||
| } | ||
|
|
||
| impl UpdateFeedCliCommand { |
There was a problem hiding this comment.
should we add a --clear-metros option?
martinsander00
left a comment
There was a problem hiding this comment.
A few pattern-based suggestions (from Karl's usual review patterns). None are correctness/security blockers — overall this looks good and cleanly scoped as Part 1. Comments inline.
| pub name: String, // 4 + len | ||
| pub reference_count: u32, // 4 - number of access passes referencing this feed | ||
| /// `exchange_pk → group_pks`. Empty ⇒ no metro restriction. | ||
| pub metros: Vec<(Pubkey, Vec<Pubkey>)>, |
There was a problem hiding this comment.
Strong/domain types over raw primitives. Consider a named type instead of the bare tuple, e.g. struct MetroGroups { exchange: Pubkey, groups: Vec<Pubkey> }. This (Pubkey, Vec<Pubkey>) shape is repeated across FeedCreateArgs, this account, the SDK command, and the CLI, and no other serviceability state models a map this way — a named type makes the (_, groups) / (exchange_pk, group_pks) destructures read as domain concepts.
| if self.metros.is_empty() { | ||
| return FeedMetroMatch::Unrestricted; | ||
| } | ||
| match self.metros.iter().find(|(ex, _)| ex == exchange) { |
There was a problem hiding this comment.
Revert on invariant violations rather than silently dedupe. .find() silently resolves a duplicate exchange key to the first entry. If metros ever contains the same exchange twice the underlying data is wrong — better to reject duplicate exchange keys in process_create_feed / process_update_feed (revert) than to silently pick one here.
| metros: value.metros.clone(), | ||
| }; | ||
|
|
||
| try_acc_create( |
There was a problem hiding this comment.
Program logs for state changes. There's no success msg! on create (the Created: log inside try_acc_create is #[cfg(test)]-gated), and update/delete log nothing either. Consider an explicit log for these state changes, and assert the exact log line in the integration tests.
| "Invalid GlobalState Account Owner" | ||
| ); | ||
| assert_eq!( | ||
| *system_program.unsigned_key(), |
There was a problem hiding this comment.
Redundant/inconsistent check. create asserts the system-program id but update/delete don't — inconsistent within the PR. Per programs/CLAUDE.md this check is unnecessary (the system interface builds its instruction with the system program as program-id and reverts automatically), so dropping it here to match update/delete and leaning on the revert would be more consistent. Minor.
| ) -> eyre::Result<()> { | ||
| let feeds = client.list_feed(ListFeedCommand)?; | ||
|
|
||
| let mut displays: Vec<FeedDisplay> = feeds |
There was a problem hiding this comment.
Nit: prefer turbofish at the callsite over annotating the binding — .collect::<Vec<FeedDisplay>>() (or just let the following sort_by infer it) rather than let mut displays: Vec<FeedDisplay> =.
Stack — review/merge bottom-up. Replaces #3952.
SetAccessPassFeeds---## Summary
Feedaccount: ametro(exchange) → group-setcatalog, withcreate/update/deletemanaged by a catalog admin (FEED_AUTHORITYPermission orFOUNDATION). A feed with an empty metro map imposes no restriction.doublezero feedCLI (create/update/get/list/delete).AccessPass/EdgeSeat changes here.Testing Verification
Feed::groups_forunit tests (covered / not-covered / unrestricted) and borsh round-trip; SDK command-builder tests; CLI--metro EXCHANGE=GROUP,GROUPparsing.