diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc0cc36db..22c48d23b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ All notable changes to this project will be documented in this file. ### Changes +- CLI + - `doublezero feed create --permissionless` marks a feed as offered without an access grant, and `doublezero feed update --permissionless true|false` flips it afterwards. `feed list` gains a `permissionless` column, in the table and both JSON forms. The update flag takes a value rather than being a bare presence flag so that turning it off is distinguishable from leaving it alone: `FeedUpdateArgs` is compared against its default to reject a no-op update, and a bare `false` would be indistinguishable from one. (malbeclabs/infra#2390) +- Serviceability + - `Feed` carries a trailing `permissionless: bool`, set by `CreateFeed` (112) and changed by `UpdateFeed` (113). It is declarative: no instruction reads it, no gate consults it, and the paid gate for a feed is still the EdgeSeat FeedSeat. It exists so the storefront can read whether a feed is offered without a grant from the ledger instead of from a hand-maintained file, which is what a serviceability flag can honestly answer — this program has no purchase path to gate. Old accounts decode as `false` through the existing per-field `unwrap_or_default()` in `TryFrom<&[u8]>`, and old create encodings without the trailing byte decode as `false` through borsh-incremental, so no migration and no backfill is required. Deploy ordering (RFC-1): the program must deploy to all clusters before any client that emits the flag — an old program ignores the trailing byte rather than failing, so a new CLI against an old program would silently create a feed with the flag dropped. Deploy note: an existing feed grows by one byte the first time any `UpdateFeed` writes it, including a `--name`-only update that never mentions the flag, and the rent delta is charged to the signer. (malbeclabs/infra#2390) +- SDK + - The Go, Python and TypeScript `Feed` decoders read the new trailing `permissionless` byte, defaulting to `false` past end-of-input so an account written before the field decodes unchanged. `CreateFeedCommand` gains `permissionless: bool` and `UpdateFeedCommand` gains `permissionless: Option`. (malbeclabs/infra#2390) + ## [v0.38.0](https://github.com/malbeclabs/doublezero/compare/client/v0.37.0...client/v0.38.0) - 2026-08-28 ### Breaking diff --git a/crates/doublezero-daemon-cli/src/connect.rs b/crates/doublezero-daemon-cli/src/connect.rs index eec0c5285b..75b4c7c926 100644 --- a/crates/doublezero-daemon-cli/src/connect.rs +++ b/crates/doublezero-daemon-cli/src/connect.rs @@ -2682,6 +2682,7 @@ mod tests { name: code.to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, } } @@ -3504,6 +3505,7 @@ mod tests { name: code.to_string(), exchange, groups, + permissionless: false, }; self.feeds.lock().unwrap().insert(pk, feed); pk diff --git a/crates/doublezero-serviceability-instruction/src/feed.rs b/crates/doublezero-serviceability-instruction/src/feed.rs index 16ecf46051..0913cb4a00 100644 --- a/crates/doublezero-serviceability-instruction/src/feed.rs +++ b/crates/doublezero-serviceability-instruction/src/feed.rs @@ -106,6 +106,7 @@ mod tests { name: "Feed".to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }; let ix = create_feed(&pid, &payer, args); assert_eq!(ix.data[0], 112); @@ -143,6 +144,7 @@ mod tests { FeedUpdateArgs { name: Some("Feed".to_string()), groups: None, + permissionless: None, }, ); assert_eq!(update.data[0], 113); diff --git a/sdk/serviceability/python/serviceability/state.py b/sdk/serviceability/python/serviceability/state.py index 9e3a32c2b4..a5521ba9b3 100644 --- a/sdk/serviceability/python/serviceability/state.py +++ b/sdk/serviceability/python/serviceability/state.py @@ -1039,7 +1039,7 @@ def from_bytes(cls, data: bytes) -> Tenant: @dataclass class FeedSeat: - """One purchased SKU seat on an EdgeSeat access pass, carrying a feed's whole billing state. + """One purchased feed's seat on an EdgeSeat access pass, carrying a feed's whole billing state. The cap is ``max_users`` before ``window_end`` and ``max_future_users`` from ``window_end`` until ``terminates_at``, when the feed is removed from the pass. ``current_users`` is the live @@ -1242,7 +1242,7 @@ def from_bytes(cls, data: bytes) -> TopologyInfo: @dataclass class Feed: - """Serviceability catalog entry: one SKU scoped to a single metro (exchange), holding the + """Serviceability catalog entry: one feed scoped to a single metro (exchange), holding the multicast groups joinable there. One feed_key is one feed in one metro. """ @@ -1253,6 +1253,8 @@ class Feed: name: str = "" exchange: Pubkey = Pubkey.default() groups: list[Pubkey] = field(default_factory=list) + # Declarative catalog label; false on an account written before the field existed. + permissionless: bool = False pub_key: Pubkey = Pubkey.default() # set from account address after deserialization @classmethod @@ -1267,4 +1269,5 @@ def from_bytes(cls, data: bytes) -> Feed: # A feed serves one metro: an exchange pubkey followed by a Vec of joinable groups. f.exchange = _read_pubkey(r) f.groups = _read_pubkey_vec(r) + f.permissionless = r.read_bool() return f diff --git a/sdk/serviceability/python/serviceability/tests/test_fixtures.py b/sdk/serviceability/python/serviceability/tests/test_fixtures.py index b7cc5c588a..c1caeeddb4 100644 --- a/sdk/serviceability/python/serviceability/tests/test_fixtures.py +++ b/sdk/serviceability/python/serviceability/tests/test_fixtures.py @@ -582,6 +582,7 @@ def test_deserialize(self): "GroupsLen": len(feed.groups), "Group0": feed.groups[0], "Group1": feed.groups[1], + "Permissionless": feed.permissionless, }, ) assert feed.account_type == 18 @@ -589,6 +590,16 @@ def test_deserialize(self): assert feed.code == "shreds" assert feed.name == "Shreds" assert len(feed.groups) == 2 + assert feed.permissionless is True + + def test_permissionless_defaults_false_on_a_pre_flag_account(self): + # An account written before the flag existed lacks the trailing byte; it reads false, + # matching the Rust program's TryFrom unwrap_or_default. + data, _ = _load_fixture("feed") + feed = Feed.from_bytes(data[:-1]) + assert feed.permissionless is False + assert feed.code == "shreds" + assert len(feed.groups) == 2 class TestFixtureAccessPassLegacyCapDefaults: diff --git a/sdk/serviceability/testdata/fixtures/feed.bin b/sdk/serviceability/testdata/fixtures/feed.bin index c0f93f1c70..07d300b597 100644 Binary files a/sdk/serviceability/testdata/fixtures/feed.bin and b/sdk/serviceability/testdata/fixtures/feed.bin differ diff --git a/sdk/serviceability/testdata/fixtures/feed.json b/sdk/serviceability/testdata/fixtures/feed.json index 395c248a9c..ce972c4b38 100644 --- a/sdk/serviceability/testdata/fixtures/feed.json +++ b/sdk/serviceability/testdata/fixtures/feed.json @@ -46,6 +46,11 @@ "name": "Group1", "value": "GH7YkRi9soP4j2JAUYTMEMBtkDFyGf7oYu1aGGRucd5H", "typ": "pubkey" + }, + { + "name": "Permissionless", + "value": "true", + "typ": "bool" } ] } \ No newline at end of file diff --git a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs index 7c406f0e3d..d4ca5a0c6f 100644 --- a/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs +++ b/sdk/serviceability/testdata/fixtures/generate-fixtures/src/main.rs @@ -1491,7 +1491,8 @@ fn generate_access_pass_edge_seat(dir: &Path) { } /// Borsh-encoded `Feed` account. Field order: account_type, owner, bump_seed, code, name, -/// exchange (Pubkey), groups (Vec). Two groups, so the vec decoding is exercised. +/// exchange (Pubkey), groups (Vec), permissionless (bool). Two groups, so the vec +/// decoding is exercised; permissionless is true so a decoder that skips the byte is caught. fn generate_feed(dir: &Path) { let owner = pubkey_from_byte(0xE0); let exchange = pubkey_from_byte(0xE1); @@ -1506,6 +1507,7 @@ fn generate_feed(dir: &Path) { name: "Shreds".into(), exchange, groups: vec![group0, group1], + permissionless: true, }; let data = borsh::to_vec(&val).unwrap(); @@ -1523,6 +1525,7 @@ fn generate_feed(dir: &Path) { FieldValue { name: "GroupsLen".into(), value: "2".into(), typ: "u32".into() }, FieldValue { name: "Group0".into(), value: pubkey_bs58(&group0), typ: "pubkey".into() }, FieldValue { name: "Group1".into(), value: pubkey_bs58(&group1), typ: "pubkey".into() }, + FieldValue { name: "Permissionless".into(), value: "true".into(), typ: "bool".into() }, ], }; diff --git a/sdk/serviceability/typescript/serviceability/state.ts b/sdk/serviceability/typescript/serviceability/state.ts index 76e190feca..ee0f122132 100644 --- a/sdk/serviceability/typescript/serviceability/state.ts +++ b/sdk/serviceability/typescript/serviceability/state.ts @@ -1053,7 +1053,7 @@ export const ACCESS_PASS_TYPE_SOLANA_RPC = 2; export const ACCESS_PASS_TYPE_OTHERS = 3; export const ACCESS_PASS_TYPE_EDGE_SEAT = 4; -// One purchased SKU seat on an EdgeSeat access pass, carrying a feed's whole billing state. The cap +// One purchased feed's seat on an EdgeSeat access pass, carrying a feed's whole billing state. The cap // is maxUsers before windowEnd and maxFutureUsers from windowEnd until terminatesAt, when the feed // is removed from the pass. currentUsers is the live count. anniversaryDay is the original start // day-of-month (1..=31) for drift-free renewals. windowEnd and terminatesAt are unix seconds. @@ -1236,7 +1236,7 @@ export function deserializePermission(data: Uint8Array): Permission { // Feed // --------------------------------------------------------------------------- -// Serviceability catalog entry: one SKU scoped to a single metro (exchange), holding the +// Serviceability catalog entry: one feed scoped to a single metro (exchange), holding the // multicast groups joinable there. One feed_key is one feed in one metro. export interface Feed { accountType: number; @@ -1246,6 +1246,8 @@ export interface Feed { name: string; exchange: PublicKey; groups: PublicKey[]; + /** Declarative catalog label; false on an account written before the field existed. */ + permissionless: boolean; } export function deserializeFeed(data: Uint8Array): Feed { @@ -1258,6 +1260,7 @@ export function deserializeFeed(data: Uint8Array): Feed { // A feed serves one metro: an exchange pubkey followed by a Vec of joinable groups. const exchange = readPubkey(r); const groups = readPubkeyVec(r); + const permissionless = r.readBool(); return { accountType, owner, @@ -1266,5 +1269,6 @@ export function deserializeFeed(data: Uint8Array): Feed { name, exchange, groups, + permissionless, }; } diff --git a/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts index 4da608ede0..ee005ae738 100644 --- a/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts +++ b/sdk/serviceability/typescript/serviceability/tests/fixtures.test.ts @@ -597,6 +597,7 @@ describe("Feed fixture", () => { GroupsLen: feed.groups.length, Group0: feed.groups[0], Group1: feed.groups[1], + Permissionless: feed.permissionless, }); expect(feed.accountType).toBe(18); @@ -604,6 +605,17 @@ describe("Feed fixture", () => { expect(feed.code).toBe("shreds"); expect(feed.name).toBe("Shreds"); expect(feed.groups).toHaveLength(2); + expect(feed.permissionless).toBe(true); + }); + + test("permissionless defaults false on a pre-flag account", () => { + // An account written before the flag existed lacks the trailing byte; it reads false, + // matching the Rust program's TryFrom unwrap_or_default. + const [data] = loadFixture("feed"); + const feed = deserializeFeed(data.slice(0, -1)); + expect(feed.permissionless).toBe(false); + expect(feed.code).toBe("shreds"); + expect(feed.groups).toHaveLength(2); }); }); diff --git a/smartcontract/cli/src/accesspass/get.rs b/smartcontract/cli/src/accesspass/get.rs index cd31005d1f..e2aa895591 100644 --- a/smartcontract/cli/src/accesspass/get.rs +++ b/smartcontract/cli/src/accesspass/get.rs @@ -457,6 +457,7 @@ mod tests { name: "QA Payments".to_string(), exchange: exchange_key, groups: vec![group_key], + permissionless: false, }; let mgroup = MulticastGroup { @@ -744,6 +745,7 @@ mod tests { name: "Lashay 1".to_string(), exchange: *exchange, groups: vec![], + permissionless: false, }) .collect(); @@ -848,6 +850,7 @@ mod tests { name: "QA Payments".to_string(), exchange: exchange_key, groups: vec![], + permissionless: false, }; client diff --git a/smartcontract/cli/src/accesspass/list.rs b/smartcontract/cli/src/accesspass/list.rs index 36188dd5ba..c0bbc0aa26 100644 --- a/smartcontract/cli/src/accesspass/list.rs +++ b/smartcontract/cli/src/accesspass/list.rs @@ -976,6 +976,7 @@ mod tests { name: "QA Payments".to_string(), exchange: Pubkey::new_unique(), groups: vec![feed_group_pubkey], + permissionless: false, }; let accesspass = AccessPass { diff --git a/smartcontract/cli/src/exchange/resolve.rs b/smartcontract/cli/src/exchange/resolve.rs index 3658dddf22..4432e2032d 100644 --- a/smartcontract/cli/src/exchange/resolve.rs +++ b/smartcontract/cli/src/exchange/resolve.rs @@ -108,6 +108,7 @@ mod tests { name: "QA Payments".to_string(), exchange: Pubkey::new_unique(), groups: vec![], + permissionless: false, }; client diff --git a/smartcontract/cli/src/feed/create.rs b/smartcontract/cli/src/feed/create.rs index d8a0ee81f3..b8d9d3ad3a 100644 --- a/smartcontract/cli/src/feed/create.rs +++ b/smartcontract/cli/src/feed/create.rs @@ -23,6 +23,9 @@ pub struct CreateFeedCliCommand { /// Multicast group pubkey or code joinable in this metro (repeatable) #[arg(long = "group", value_parser = validate_pubkey_or_code, num_args = 1..)] pub groups: Vec, + /// Offer this feed without an access grant. Off unless given. + #[arg(long, default_value_t = false)] + pub permissionless: bool, } impl CreateFeedCliCommand { @@ -50,6 +53,7 @@ impl CreateFeedCliCommand { name: self.name, exchange, groups, + permissionless: self.permissionless, })?; print_signature(out, &signature) @@ -142,6 +146,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk, groups: vec![group_pk], + permissionless: false, })) .times(1) .returning(move |_| Ok((signature, feed_pk))); @@ -154,6 +159,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk.to_string(), groups: vec![group_pk.to_string()], + permissionless: false, } .execute(&ctx, &client, &mut output), ); @@ -199,6 +205,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk, groups: vec![group_pk], + permissionless: false, })) .times(1) .returning(move |_| Ok((signature, feed_pk))); @@ -211,6 +218,7 @@ mod tests { name: "Feed".to_string(), exchange: "xchi".to_string(), groups: vec!["mg01".to_string()], + permissionless: false, } .execute(&ctx, &client, &mut output), ); @@ -243,6 +251,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk.to_string(), groups: vec!["nope".to_string()], + permissionless: false, } .execute(&ctx, &client, &mut output), ); @@ -270,6 +279,7 @@ mod tests { name: "Feed".to_string(), exchange: "nope".to_string(), groups: vec![], + permissionless: false, } .execute(&ctx, &client, &mut output), ); diff --git a/smartcontract/cli/src/feed/delete.rs b/smartcontract/cli/src/feed/delete.rs index bd2c0dfda1..bc9f65ab13 100644 --- a/smartcontract/cli/src/feed/delete.rs +++ b/smartcontract/cli/src/feed/delete.rs @@ -206,6 +206,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk, groups: vec![], + permissionless: false, }; let feed_for_get = feed.clone(); client diff --git a/smartcontract/cli/src/feed/guard.rs b/smartcontract/cli/src/feed/guard.rs index 566676bf42..7321a80cfb 100644 --- a/smartcontract/cli/src/feed/guard.rs +++ b/smartcontract/cli/src/feed/guard.rs @@ -394,6 +394,7 @@ pub(crate) mod fixtures { name: "Feed".to_string(), exchange, groups, + permissionless: false, } } diff --git a/smartcontract/cli/src/feed/list.rs b/smartcontract/cli/src/feed/list.rs index bc4b90aba4..312e57e52f 100644 --- a/smartcontract/cli/src/feed/list.rs +++ b/smartcontract/cli/src/feed/list.rs @@ -40,6 +40,7 @@ pub struct FeedDisplay { pub exchange: String, pub groups: usize, pub group_codes: String, + pub permissionless: bool, #[serde(serialize_with = "serializer::serialize_pubkey_as_string")] pub owner: Pubkey, } @@ -85,6 +86,7 @@ impl ListFeedCliCommand { }) .collect::>() .join(", "), + permissionless: feed.permissionless, owner: feed.owner, }) .collect::>(); @@ -130,6 +132,7 @@ mod tests { name: "QA Payments".to_string(), exchange: exchange_pk, groups: vec![mgroup_pk, unknown_mgroup_pk], + permissionless: false, }; client.expect_list_feed().returning(move |_| { let mut feeds = HashMap::new(); @@ -194,7 +197,7 @@ mod tests { let output_str = String::from_utf8(output).unwrap(); assert_eq!( output_str, - " account | code | name | exchange | groups | group_codes | owner \n 1111111FVAiSujNZVgYSc27t6zUTWoKfAGxbRzzPR | qa-payments | QA Payments | xams | 2 | mg01, 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo4 | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo9 \n" + " account | code | name | exchange | groups | group_codes | permissionless | owner \n 1111111FVAiSujNZVgYSc27t6zUTWoKfAGxbRzzPR | qa-payments | QA Payments | xams | 2 | mg01, 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo4 | false | 11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo9 \n" ); } @@ -214,6 +217,7 @@ mod tests { name: code.to_string(), exchange, groups: vec![], + permissionless: false, }; client.expect_list_feed().returning(move |_| { diff --git a/smartcontract/cli/src/feed/resolve.rs b/smartcontract/cli/src/feed/resolve.rs index a7d005b24b..b80c86f5a6 100644 --- a/smartcontract/cli/src/feed/resolve.rs +++ b/smartcontract/cli/src/feed/resolve.rs @@ -126,6 +126,7 @@ mod tests { name: code.to_string(), exchange, groups: vec![], + permissionless: false, } } diff --git a/smartcontract/cli/src/feed/update.rs b/smartcontract/cli/src/feed/update.rs index f3645ce48c..cba268bba7 100644 --- a/smartcontract/cli/src/feed/update.rs +++ b/smartcontract/cli/src/feed/update.rs @@ -33,6 +33,10 @@ pub struct UpdateFeedCliCommand { /// outside their access pass's feeds fails and changes nothing. #[arg(long, default_value_t = false)] pub force_unsubscribe: bool, + /// Offer this feed without an access grant, or stop offering it. Takes a value so that + /// turning the flag off is distinguishable from leaving it alone. + #[arg(long)] + pub permissionless: Option, } impl UpdateFeedCliCommand { @@ -88,6 +92,7 @@ impl UpdateFeedCliCommand { pubkey, name: self.name, groups, + permissionless: self.permissionless, })?; print_signature(out, &signature) @@ -142,6 +147,7 @@ mod tests { name: None, groups: vec![g1.to_string()], force_unsubscribe: false, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -190,6 +196,7 @@ mod tests { pubkey: f.feed_pk, name: None, groups: Some(vec![g1]), + permissionless: None, })) .times(1) .returning(move |_| Ok(signature)); @@ -204,6 +211,7 @@ mod tests { name: None, groups: vec![g1.to_string()], force_unsubscribe: true, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -252,6 +260,7 @@ mod tests { pubkey: f.feed_pk, name: None, groups: Some(vec![g1]), + permissionless: None, })) .times(1) .returning(move |_| Ok(signature)); @@ -266,6 +275,7 @@ mod tests { name: None, groups: vec![g1.to_string()], force_unsubscribe: true, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -294,6 +304,7 @@ mod tests { pubkey: f.feed_pk, name: None, groups: Some(vec![g1, g2]), + permissionless: None, })) .times(1) .returning(move |_| Ok(signature)); @@ -308,6 +319,7 @@ mod tests { name: None, groups: vec![g1.to_string(), g2.to_string()], force_unsubscribe: false, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -366,6 +378,7 @@ mod tests { name: None, groups: vec![g1.to_string()], force_unsubscribe: true, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -434,6 +447,7 @@ mod tests { name: None, groups: vec![g1.to_string()], force_unsubscribe: true, + permissionless: None, } .execute(&ctx, &client, &mut output), ); @@ -491,6 +505,7 @@ mod tests { name: "Feed".to_string(), exchange: exchange_pk, groups: vec![], + permissionless: false, }; let feed_for_get = feed.clone(); client @@ -541,6 +556,7 @@ mod tests { pubkey: feed_pk, name: Some("Feed v2".to_string()), groups: Some(vec![group_pk]), + permissionless: None, })) .times(1) .returning(move |_| Ok(signature)); @@ -555,6 +571,7 @@ mod tests { name: Some("Feed v2".to_string()), groups: vec!["mg01".to_string()], force_unsubscribe: false, + permissionless: None, } .execute(&ctx, &client, &mut output), ); diff --git a/smartcontract/cli/src/user/create_subscribe.rs b/smartcontract/cli/src/user/create_subscribe.rs index ba7c8f9651..7e4eaabf21 100644 --- a/smartcontract/cli/src/user/create_subscribe.rs +++ b/smartcontract/cli/src/user/create_subscribe.rs @@ -503,6 +503,7 @@ mod tests { name: "Shreds NYC".to_string(), exchange: Pubkey::new_unique(), groups: vec![], + permissionless: false, }; client .expect_get_feed() diff --git a/smartcontract/cli/src/user/get.rs b/smartcontract/cli/src/user/get.rs index 0c67c1bc34..047ce31472 100644 --- a/smartcontract/cli/src/user/get.rs +++ b/smartcontract/cli/src/user/get.rs @@ -476,6 +476,7 @@ mod tests { name: "QA Payments".to_string(), exchange: exchange_key, groups: vec![group_pubkey], + permissionless: false, }; let exchange = Exchange { diff --git a/smartcontract/programs/doublezero-serviceability/src/instructions.rs b/smartcontract/programs/doublezero-serviceability/src/instructions.rs index a68995bbaa..ab8b0e0f6e 100644 --- a/smartcontract/programs/doublezero-serviceability/src/instructions.rs +++ b/smartcontract/programs/doublezero-serviceability/src/instructions.rs @@ -1390,6 +1390,7 @@ mod tests { name: "Shreds".to_string(), exchange: Pubkey::new_unique(), groups: vec![Pubkey::new_unique()], + permissionless: true, }), "CreateFeed", ); @@ -1397,6 +1398,7 @@ mod tests { DoubleZeroInstruction::UpdateFeed(FeedUpdateArgs { name: Some("Shreds".to_string()), groups: Some(vec![Pubkey::new_unique()]), + permissionless: Some(true), }), "UpdateFeed", ); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/feed/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/feed/create.rs index 65e21eefcc..07a5f3ef9e 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/feed/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/feed/create.rs @@ -34,6 +34,11 @@ pub struct FeedCreateArgs { pub exchange: Pubkey, /// Multicast groups joinable in this metro. pub groups: Vec, + /// Offer the feed without an access grant. Trailing and incremental, so a client built + /// before this field existed sends a shorter buffer and gets `false` — the same answer the + /// account gives for a feed created then. + #[incremental(default = false)] + pub permissionless: bool, } pub fn process_create_feed( @@ -93,6 +98,7 @@ pub fn process_create_feed( name: value.name.clone(), exchange: value.exchange, groups: value.groups.clone(), + permissionless: value.permissionless, }; try_acc_create( diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/feed/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/feed/update.rs index 243d9e338d..de5eebd69a 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/feed/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/feed/update.rs @@ -14,12 +14,17 @@ use solana_program::{ pubkey::Pubkey, }; -/// `code` and `exchange` are the PDA seeds and therefore immutable; only `name` and the group set -/// are mutable. +/// `code` and `exchange` are the PDA seeds and therefore immutable; `name`, the group set and +/// the permissionless flag are mutable. #[derive(BorshSerialize, BorshDeserializeIncremental, PartialEq, Debug, Clone, Default)] pub struct FeedUpdateArgs { pub name: Option, pub groups: Option>, + /// `Option`, not a bare `bool`, because of the no-op guard below: a bare `false` would be + /// indistinguishable from `FeedUpdateArgs::default()` and rejected, leaving no way to turn + /// the flag back off. + #[incremental(default = None)] + pub permissionless: Option, } pub fn process_update_feed( @@ -70,6 +75,9 @@ pub fn process_update_feed( if let Some(ref groups) = value.groups { feed.groups = groups.clone(); } + if let Some(permissionless) = value.permissionless { + feed.permissionless = permissionless; + } try_acc_write(&feed, feed_account, payer_account, accounts)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/state/feed.rs b/smartcontract/programs/doublezero-serviceability/src/state/feed.rs index 6832019906..a5bc56652a 100644 --- a/smartcontract/programs/doublezero-serviceability/src/state/feed.rs +++ b/smartcontract/programs/doublezero-serviceability/src/state/feed.rs @@ -6,13 +6,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; use std::fmt; -/// A serviceability catalog entry: one SKU scoped to a single metro (`exchange`), holding the +/// A serviceability catalog entry: one feed scoped to a single metro (`exchange`), holding the /// multicast groups joinable there. /// -/// The pubkey of this account (`feed_key`) is the SKU identifier carried on EdgeSeat access passes. -/// `code` and `exchange` are the PDA seeds, so both are immutable; `name` and `groups` are mutable. -/// One `feed_key` is one feed in one metro (e.g. `shreds@tokyo`); a different metro is a -/// different feed account. +/// The pubkey of this account is the `feed_key` carried on EdgeSeat access passes. +/// `code` and `exchange` are the PDA seeds, so both are immutable; `name`, `groups` and +/// `permissionless` are mutable. One `feed_key` is one feed in one metro (e.g. `shreds@tokyo`); +/// a different metro is a different feed account. Every account sharing a `code` is one SKU +/// (malbeclabs/infra#2390) — a readability term for the storefront, not something stored here. #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Feed { @@ -30,6 +31,11 @@ pub struct Feed { pub name: String, // 4 + len pub exchange: Pubkey, // 32 (PDA seed, immutable) - the metro this feed serves pub groups: Vec, // 4 + 32*len - multicast groups joinable in this metro + /// Whether this feed is offered without an access grant. Declarative: no instruction reads + /// it, and the paid gate is still the EdgeSeat FeedSeat. It is a catalog label the storefront + /// reads back from `feed list`, so `false` on an account written before this field existed is + /// the correct answer, not a missing one. + pub permissionless: bool, // 1 } impl Feed { @@ -48,14 +54,15 @@ impl fmt::Display for Feed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "account_type: {}, owner: {}, bump_seed: {}, code: {}, name: {}, exchange: {}, groups: {}", + "account_type: {}, owner: {}, bump_seed: {}, code: {}, name: {}, exchange: {}, groups: {}, permissionless: {}", self.account_type, self.owner, self.bump_seed, self.code, self.name, self.exchange, - self.groups.len() + self.groups.len(), + self.permissionless ) } } @@ -72,6 +79,8 @@ impl TryFrom<&[u8]> for Feed { name: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), exchange: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), groups: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), + // EOF on an account written before this field existed, which reads false. + permissionless: BorshDeserialize::deserialize(&mut data).unwrap_or_default(), }; if out.account_type != AccountType::Feed { @@ -118,6 +127,7 @@ mod tests { name: "Shreds".to_string(), exchange, groups, + permissionless: false, } } @@ -153,4 +163,53 @@ mod tests { let data = borsh::to_vec(&val).unwrap(); assert!(Feed::try_from(&data[..]).is_err()); } + + /// A Feed written before `permissionless` existed still decodes, reading false — the + /// per-field `unwrap_or_default()` in `TryFrom<&[u8]>` is what makes the 151 accounts + /// already on mainnet need no migration. + #[test] + fn test_feed_backward_compat_no_permissionless() { + #[derive(BorshSerialize)] + struct LegacyFeed { + account_type: AccountType, + owner: Pubkey, + bump_seed: u8, + code: String, + name: String, + exchange: Pubkey, + groups: Vec, + } + + let exchange = Pubkey::new_unique(); + let group = Pubkey::new_unique(); + let legacy = LegacyFeed { + account_type: AccountType::Feed, + owner: Pubkey::new_unique(), + bump_seed: 7, + code: "shreds".to_string(), + name: "Shreds".to_string(), + exchange, + groups: vec![group], + }; + + let bytes = borsh::to_vec(&legacy).unwrap(); + let decoded = Feed::try_from(&bytes[..]).unwrap(); + + assert!(!decoded.permissionless); + // The fields before it still land where they should: a decoder that mis-read the + // missing byte would corrupt these rather than only the flag. + assert_eq!(decoded.code, "shreds"); + assert_eq!(decoded.exchange, exchange); + assert_eq!(decoded.groups, vec![group]); + } + + /// The flag round-trips when it is set, so the trailing byte is really written and read + /// rather than always defaulting. + #[test] + fn test_feed_permissionless_round_trip() { + let mut val = feed_with(Pubkey::new_unique(), vec![Pubkey::new_unique()]); + val.permissionless = true; + let data = borsh::to_vec(&val).unwrap(); + assert!(Feed::try_from(&data[..]).unwrap().permissionless); + } } diff --git a/smartcontract/programs/doublezero-serviceability/tests/feed_metro_gate_test.rs b/smartcontract/programs/doublezero-serviceability/tests/feed_metro_gate_test.rs index cd28d39764..903c089e14 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/feed_metro_gate_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/feed_metro_gate_test.rs @@ -286,6 +286,7 @@ async fn create_feed( name: code.to_string(), exchange, groups, + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), diff --git a/smartcontract/programs/doublezero-serviceability/tests/feed_subscription_test.rs b/smartcontract/programs/doublezero-serviceability/tests/feed_subscription_test.rs index 48f7364c4e..bac5e6fc3c 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/feed_subscription_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/feed_subscription_test.rs @@ -227,6 +227,7 @@ async fn create_feed(f: &mut Fixture, code: &str, exchange: Pubkey, groups: Vec< name: code.to_string(), exchange, groups, + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), diff --git a/smartcontract/programs/doublezero-serviceability/tests/feed_test.rs b/smartcontract/programs/doublezero-serviceability/tests/feed_test.rs index 1589dfa55e..2f48f5dae3 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/feed_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/feed_test.rs @@ -109,6 +109,7 @@ async fn test_feed_create_get_update_delete() { name: "Shreds".to_string(), exchange, groups: groups.clone(), + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -139,6 +140,7 @@ async fn test_feed_create_get_update_delete() { DoubleZeroInstruction::UpdateFeed(FeedUpdateArgs { name: Some("Shreds v2".to_string()), groups: Some(new_groups.clone()), + permissionless: None, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -197,6 +199,7 @@ async fn test_feed_same_code_different_exchange_allowed() { name: "Shreds".to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -235,6 +238,7 @@ async fn test_feed_create_duplicate_rejected() { name: name.to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }) }; @@ -288,6 +292,7 @@ async fn test_feed_create_empty_groups_rejected() { name: "Empty".to_string(), exchange, groups: vec![], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -318,6 +323,7 @@ async fn test_feed_create_duplicate_group_rejected() { name: "Dup group".to_string(), exchange, groups: vec![group, group], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -347,6 +353,7 @@ async fn test_feed_create_default_exchange_rejected() { name: "No default".to_string(), exchange: Pubkey::default(), groups: vec![Pubkey::new_unique()], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -378,6 +385,7 @@ async fn test_feed_create_unauthorized_caller_rejected() { name: "Unauthorized".to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -389,3 +397,148 @@ async fn test_feed_create_unauthorized_caller_rejected() { assert_custom_at_ix0(&result, custom_code(DoubleZeroError::NotAllowed)); } + +/// The pre-flag encoding (no trailing `permissionless` byte) still decodes and creates a feed — +/// wire compatibility for a CLI that has not been upgraded yet. +/// +/// This is the case the deploy ordering exists to make safe. `borsh-incremental` substitutes the +/// default only when a field consumes zero bytes, which a trailing bool either does entirely or +/// not at all, so an old client's shorter buffer lands on `false` rather than erroring. +#[tokio::test] +async fn test_feed_create_old_encoding_without_permissionless_byte() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + let globalstate_pubkey = + init_globalstate(&mut banks_client, program_id, &payer, recent_blockhash).await; + + let exchange = Pubkey::new_unique(); + let (feed_pubkey, _) = get_feed_pda(&program_id, "legacy", &exchange); + let group = Pubkey::new_unique(); + + // Serialize the current args and strip the trailing byte to get exactly what an old client + // emits. `true` so the byte is 1 and the assert below cannot pass by coincidence. + let mut data = borsh::to_vec(&DoubleZeroInstruction::CreateFeed(FeedCreateArgs { + code: "legacy".to_string(), + name: "Legacy".to_string(), + exchange, + groups: vec![group], + permissionless: true, + })) + .unwrap(); + assert_eq!(data.pop(), Some(1), "last byte must be permissionless"); + + let accounts = vec![ + AccountMeta::new(feed_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(solana_system_interface::program::ID, false), + ]; + let instruction = + solana_sdk::instruction::Instruction::new_with_bytes(program_id, &data, accounts); + let mut tx = + solana_sdk::transaction::Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + tx.try_sign(&[&payer], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("the pre-flag encoding should still create a feed"); + + let feed = get_account_data(&mut banks_client, feed_pubkey) + .await + .expect("Unable to get Feed") + .get_feed() + .unwrap(); + assert_eq!(feed.code, "legacy".to_string()); + assert_eq!(feed.groups, vec![group]); + assert!( + !feed.permissionless, + "an omitted flag is false, not a decode failure" + ); +} + +/// The flag is settable at create and flippable both ways afterwards. +/// +/// Turning it back OFF is the case that pins `Option` on the update args: with a bare +/// `bool`, an args value carrying only `permissionless: false` would equal +/// `FeedUpdateArgs::default()` and be rejected by the no-op guard. +#[tokio::test] +async fn test_feed_permissionless_set_at_create_and_flipped_both_ways() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + let globalstate_pubkey = + init_globalstate(&mut banks_client, program_id, &payer, recent_blockhash).await; + + let exchange = Pubkey::new_unique(); + let (feed_pubkey, _) = get_feed_pda(&program_id, "open", &exchange); + let accounts = vec![ + AccountMeta::new(feed_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ]; + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreateFeed(FeedCreateArgs { + code: "open".to_string(), + name: "Open".to_string(), + exchange, + groups: vec![Pubkey::new_unique()], + permissionless: true, + }), + accounts.clone(), + &payer, + ) + .await; + + let feed = get_account_data(&mut banks_client, feed_pubkey) + .await + .unwrap() + .get_feed() + .unwrap(); + assert!(feed.permissionless); + + // Off. A name-only update would leave it alone; this one names it. + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::UpdateFeed(FeedUpdateArgs { + name: None, + groups: None, + permissionless: Some(false), + }), + accounts.clone(), + &payer, + ) + .await; + assert!( + !get_account_data(&mut banks_client, feed_pubkey) + .await + .unwrap() + .get_feed() + .unwrap() + .permissionless + ); + + // And back on. + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::UpdateFeed(FeedUpdateArgs { + name: None, + groups: None, + permissionless: Some(true), + }), + accounts, + &payer, + ) + .await; + assert!( + get_account_data(&mut banks_client, feed_pubkey) + .await + .unwrap() + .get_feed() + .unwrap() + .permissionless + ); +} diff --git a/smartcontract/programs/doublezero-serviceability/tests/set_access_pass_feeds_test.rs b/smartcontract/programs/doublezero-serviceability/tests/set_access_pass_feeds_test.rs index 6c72a1e3cc..73cf498eea 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/set_access_pass_feeds_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/set_access_pass_feeds_test.rs @@ -137,6 +137,7 @@ async fn create_feed( name: code.to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }), vec![ AccountMeta::new(feed_pubkey, false), @@ -598,6 +599,7 @@ async fn test_cannot_set_max_users_below_current_users() { name: "Live".to_string(), exchange: feed_exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, }; program_test.add_account( feed_pubkey, diff --git a/smartcontract/sdk/go/serviceability/deserialize.go b/smartcontract/sdk/go/serviceability/deserialize.go index 33dba89dfe..201a97eea9 100644 --- a/smartcontract/sdk/go/serviceability/deserialize.go +++ b/smartcontract/sdk/go/serviceability/deserialize.go @@ -512,5 +512,7 @@ func DeserializeFeed(reader *ByteReader, feed *Feed) { // groups. feed.Exchange = reader.ReadPubkey() feed.Groups = reader.ReadPubkeySlice() + // ReadU8 returns 0 past EOF, so an account written before this field reads false. + feed.Permissionless = (reader.ReadU8() != 0) // Note: feed.PubKey is set from the account address in client.go after deserialization } diff --git a/smartcontract/sdk/go/serviceability/fixture_test.go b/smartcontract/sdk/go/serviceability/fixture_test.go index 7f6931624d..dd9570608e 100644 --- a/smartcontract/sdk/go/serviceability/fixture_test.go +++ b/smartcontract/sdk/go/serviceability/fixture_test.go @@ -240,6 +240,20 @@ func TestFixtureFeed(t *testing.T) { require.Len(t, feed.Groups, 2) assert.Equal(t, byte(0xE2), feed.Groups[0][0]) assert.Equal(t, byte(0xE3), feed.Groups[1][0]) + assert.True(t, feed.Permissionless) +} + +// An account written before the flag existed lacks the trailing byte. ReadU8 returns 0 past EOF, +// so it reads false — matching the Rust program's TryFrom unwrap_or_default. +func TestFixtureFeedPermissionlessDefaultsFalse(t *testing.T) { + data, _ := loadFixture(t, "feed") + + var feed serviceability.Feed + serviceability.DeserializeFeed(serviceability.NewByteReader(data[:len(data)-1]), &feed) + + assert.False(t, feed.Permissionless) + assert.Equal(t, "shreds", feed.Code) + require.Len(t, feed.Groups, 2) } func fixtureFieldValue(t *testing.T, meta fixtureMeta, name string) string { diff --git a/smartcontract/sdk/go/serviceability/state.go b/smartcontract/sdk/go/serviceability/state.go index 155e4c01f5..794ad55e89 100644 --- a/smartcontract/sdk/go/serviceability/state.go +++ b/smartcontract/sdk/go/serviceability/state.go @@ -1138,7 +1138,7 @@ func (s AccessPassStatus) String() string { } } -// FeedSeat is one purchased SKU seat on an EdgeSeat access pass, carrying the feed's whole +// FeedSeat is one purchased feed's seat on an EdgeSeat access pass, carrying the feed's whole // billing lifecycle. FeedKey is the pubkey of the serviceability Feed account. The cap is // MaxUsers before WindowEnd and MaxFutureUsers from WindowEnd until TerminatesAt, when the feed // is removed from the pass. CurrentUsers is the live count. AnniversaryDay is the original start @@ -1434,7 +1434,7 @@ type TopologyInfo struct { PubKey [32]byte } -// Feed is a serviceability catalog entry: one SKU scoped to a single metro (Exchange), holding the +// Feed is a serviceability catalog entry: one feed scoped to a single metro (Exchange), holding the // multicast groups joinable there. One feed_key is one feed in one metro. type Feed struct { AccountType AccountType @@ -1444,5 +1444,8 @@ type Feed struct { Name string Exchange [32]byte Groups [][32]byte - PubKey [32]byte + // Permissionless is a declarative catalog label: no instruction reads it, and the paid gate + // is still the EdgeSeat FeedSeat. False on an account written before the field existed. + Permissionless bool + PubKey [32]byte } diff --git a/smartcontract/sdk/rs/src/commands/feed/create.rs b/smartcontract/sdk/rs/src/commands/feed/create.rs index 61387892e6..975235a2e2 100644 --- a/smartcontract/sdk/rs/src/commands/feed/create.rs +++ b/smartcontract/sdk/rs/src/commands/feed/create.rs @@ -13,6 +13,8 @@ pub struct CreateFeedCommand { pub exchange: Pubkey, /// Multicast groups joinable in this metro. pub groups: Vec, + /// Offer the feed without an access grant. + pub permissionless: bool, } impl CreateFeedCommand { @@ -31,6 +33,7 @@ impl CreateFeedCommand { name: self.name.clone(), exchange: self.exchange, groups: self.groups.clone(), + permissionless: self.permissionless, }, ); @@ -71,6 +74,7 @@ mod tests { name: "Test Feed".to_string(), exchange, groups: vec![group], + permissionless: false, }, ); client @@ -89,6 +93,7 @@ mod tests { name: "Test Feed".to_string(), exchange, groups: vec![group], + permissionless: false, }; let create_invalid_command = CreateFeedCommand { @@ -120,6 +125,7 @@ mod tests { name: "Test Feed".to_string(), exchange, groups: vec![group], + permissionless: false, }, ); let (permission_pda_pubkey, _) = get_permission_pda(&program_id, &payer); @@ -142,6 +148,7 @@ mod tests { name: "Test Feed".to_string(), exchange, groups: vec![group], + permissionless: false, }; let res = create_command.execute(&client); diff --git a/smartcontract/sdk/rs/src/commands/feed/get.rs b/smartcontract/sdk/rs/src/commands/feed/get.rs index 45e37fe462..9d690f38d8 100644 --- a/smartcontract/sdk/rs/src/commands/feed/get.rs +++ b/smartcontract/sdk/rs/src/commands/feed/get.rs @@ -85,6 +85,7 @@ mod tests { name: code.to_string(), exchange, groups: vec![Pubkey::new_unique()], + permissionless: false, } } diff --git a/smartcontract/sdk/rs/src/commands/feed/list.rs b/smartcontract/sdk/rs/src/commands/feed/list.rs index 10422e6daa..2d4a77711c 100644 --- a/smartcontract/sdk/rs/src/commands/feed/list.rs +++ b/smartcontract/sdk/rs/src/commands/feed/list.rs @@ -50,6 +50,7 @@ mod tests { name: "feed1_name".to_string(), exchange: Pubkey::new_unique(), groups: vec![Pubkey::new_unique()], + permissionless: false, }; let feed2_pubkey = Pubkey::new_unique(); @@ -61,6 +62,7 @@ mod tests { name: "feed2_name".to_string(), exchange: Pubkey::new_unique(), groups: vec![Pubkey::new_unique()], + permissionless: false, }; client diff --git a/smartcontract/sdk/rs/src/commands/feed/update.rs b/smartcontract/sdk/rs/src/commands/feed/update.rs index c506570913..c2b444dbec 100644 --- a/smartcontract/sdk/rs/src/commands/feed/update.rs +++ b/smartcontract/sdk/rs/src/commands/feed/update.rs @@ -9,6 +9,8 @@ pub struct UpdateFeedCommand { pub name: Option, /// Replacement multicast group set. `None` leaves the groups unchanged. pub groups: Option>, + /// `None` leaves the permissionless flag unchanged. + pub permissionless: Option, } impl UpdateFeedCommand { @@ -20,6 +22,7 @@ impl UpdateFeedCommand { FeedUpdateArgs { name: self.name.clone(), groups: self.groups.clone(), + permissionless: self.permissionless, }, ); @@ -60,6 +63,7 @@ mod tests { FeedUpdateArgs { name: Some("Test Feed".to_string()), groups: None, + permissionless: None, }, ); client @@ -73,6 +77,7 @@ mod tests { pubkey: pda_pubkey, name: Some("Test Feed".to_string()), groups: None, + permissionless: None, } .execute(&client); assert!(res.is_ok()); @@ -93,6 +98,7 @@ mod tests { FeedUpdateArgs { name: Some("Test Feed".to_string()), groups: None, + permissionless: None, }, ); let (permission_pda_pubkey, _) = get_permission_pda(&program_id, &payer); @@ -114,6 +120,7 @@ mod tests { pubkey: pda_pubkey, name: Some("Test Feed".to_string()), groups: None, + permissionless: None, } .execute(&client); assert!(res.is_ok()); diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe_feed.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe_feed.rs index c00f792ada..fdbe924c91 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe_feed.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/subscribe_feed.rs @@ -311,6 +311,7 @@ mod tests { name: code.to_string(), exchange, groups, + permissionless: false, } } diff --git a/smartcontract/sdk/rs/src/commands/multicastgroup/unsubscribe_feed.rs b/smartcontract/sdk/rs/src/commands/multicastgroup/unsubscribe_feed.rs index 07b4d6f81f..1e4255309c 100644 --- a/smartcontract/sdk/rs/src/commands/multicastgroup/unsubscribe_feed.rs +++ b/smartcontract/sdk/rs/src/commands/multicastgroup/unsubscribe_feed.rs @@ -339,6 +339,7 @@ mod tests { name: code.to_string(), exchange: Pubkey::new_unique(), groups, + permissionless: false, } }