CommunityManagement
CommunityManagement.aes holds everything about a community that is policy rather than
economics: who the moderators are, who is muted, how many tokens you need to hold to write,
and the community's free-form metadata.
One instance is deployed per community by the factory. Its owner is the community's DAO, so every setting on it changes only through a DAO vote, except muting, which moderators do directly.
Resolve it with factory.get_community_management(token_sale). Its interface is inlined in
CommunityFactory.aci.json.
Entrypoints
| Entrypoint | Signature | Flags |
|---|---|---|
init | (initial_moderator: address, minimum_token_threshold: int, is_private: bool, meta_info: CommunityManagement.meta_info) => state | stateful |
add_moderator | (moderator: address) => unit | stateful |
delete_moderator | (moderator: address) => unit | stateful |
mute_user_id | (user_id: string) => unit | stateful |
unmute_user_id | (user_id: string) => unit | stateful |
change_minimum_token_threshold | (minimum_token_threshold: int) => unit | stateful |
change_meta_info | (meta_info: CommunityManagement.meta_info) => unit | stateful |
set_owner | (owner: address) => unit | stateful |
moderator_accounts | () => Set.set(address) | |
muted_user_ids | () => Set.set(string) | |
minimum_token_threshold | () => int | |
meta_info | () => CommunityManagement.meta_info | |
owner | () => address | |
version | () => int | |
get_state | () => state |
Types
type meta_info = map(string, string)
record state =
{ owner : address
, minimum_token_threshold : int
, is_private : bool
, moderator_accounts : Set.set(address)
, muted_user_ids : Set.set(string)
, meta_info : meta_info }Set.set('a) is the æternity standard library set, a record wrapping a map:
record set('a) = { to_map : map('a, unit) }Decoded in JavaScript the SDK gives you a Set, so membership is a plain .has():
const moderators = await cm.moderator_accounts().then((r) => r.decodedResult);
moderators.has('ak_alice...'); // true or falseThe meta_info map
meta_info is map(string, string) with no schema, no validation and no reserved keys. The
Superhero app writes these keys when it creates a community:
| Key | What the app puts there | Where it shows up |
|---|---|---|
collection | the collection slug, e.g. word | internal |
description | free text | the token page, truncated to 150 characters in the UI and 160 for the page meta description |
website | a URL | the token page |
twitter | an X handle or URL | the token page |
All four are optional. A community created with new Map() has no metadata, and the app
falls back to a generated description.
change_meta_info replaces the whole map. There is no per-key update, so read the current
map, modify it, and write it back through a ChangeMetaInfo(map(string, string)) DAO vote:
const current = await cm.meta_info().then((r) => r.decodedResult);
current.set('description', 'A new description');
// change_meta_info is owner-gated: submit `current` as a ChangeMetaInfo vote subject.Events
datatype event =
ChangeMinimumTokenThreshold(int)
| AddModerator(address)
| DeleteModerator(address)
| MuteUserId(string)
| UnmuteUserId(string)
| SetOwner(address)
| ChangedMetaInfoChangedMetaInfo is the only event in the Superhero suite that carries no arguments. It
signals that the map changed without saying how. An indexer that wants the new metadata
re-reads meta_info() when it sees this event.
Who may call what
| Action | Caller |
|---|---|
add_moderator, delete_moderator | the owner (the DAO), via an AddModerator / DeleteModerator vote |
change_minimum_token_threshold | the owner, via a ChangeMinimumTokenThreshold vote |
change_meta_info | the owner, via a ChangeMetaInfo vote |
set_owner | the owner; this is how a ChangeDAO migration hands over control |
mute_user_id, unmute_user_id | a moderator, directly |
Two guard functions enforce that table:
function require_owner() =
require(Call.caller == state.owner, "ONLY_OWNER_CAN_CHANGE")
function require_moderator() =
require(Set.member(Call.caller, state.moderator_accounts), "ONLY_MODERATOR_CAN_CHANGE")Moderators are not owners: a moderator cannot add another moderator, change the
threshold, or touch the metadata. The owner is not automatically a moderator either, so the DAO
cannot mute anyone without first voting itself onto the moderator set. The initial moderator
is the community creator, set once in init; the owner becomes the DAO immediately afterwards.
Muting is the one action that does not need a vote: governance sets the policy and appoints the people, and those people act in real time.
Initial values at creation
The factory deploys community management with fixed initial settings:
Chain.create(Call.caller, 10 ^ decimals(), is_private, meta_info) : CommunityManagement| Field | Initial value |
|---|---|
initial_moderator | Call.caller, whoever created the community |
minimum_token_threshold | 10 ^ 18, i.e. exactly one whole token |
is_private | whatever you passed to create_community |
meta_info | whatever you passed to create_community |
owner | Call.caller at construction, then the DAO |
The default threshold is a literal in CommunityFactory.create_community, not a parameter, so
every community starts at one whole token. Only a ChangeMinimumTokenThreshold vote moves it.
What minimum_token_threshold gates
On chain, it gates proposal creation. DAO.add_vote calls
community_management.minimum_token_threshold() and requires the proposer's balance to meet
it, aborting with ADD_VOTE_TOKEN_HOLDER_MISSMATCH otherwise. No other contract reads it.
Off chain, it gates the community's chat room. The Superhero API mirrors the on-chain state and uses the threshold as the membership predicate for the community's token-gated NIP-29 room: a holder is eligible when their raw balance is at least the raw threshold (base units compared to base units, no decimal shifting) and their address is not muted. A muted holder is ineligible however many tokens they hold.
What a user_id is
mute_user_id and unmute_user_id take a string, not an address, and the contract
places no constraint on the format. Superhero's chat layer compares entries from
muted_user_ids against æternity account addresses, so write ak_... addresses if you
want a mute to take effect in Superhero. A client reading the list should still handle other
strings rather than assuming the prefix.
What is_private controls
On chain, is_private is set once in init and not read by any entrypoint. It has no
dedicated getter; read it through get_state(). Contract state is public, so a private
community's moderators, mutes, threshold and metadata are readable by anyone.
Off chain, the Superhero API copies it to the community's chat room and uses it to choose the
visibility tag on the NIP-29 edit-metadata event, public or private. The web client
reports a private room as auth-required rather than rendering it openly. A token with no
chat room defaults to public.
is_private is a chat-room visibility preference stored on chain, not a confidentiality
control over on-chain data.
Related
- DAO: the owner of this contract and the source of every governed change
- CommunityFactory: where the initial values above are set
- Moderators: the moderator role explained for community members
- Moderation: what muting does in the social layer
- Proposals: the vote subjects that change these settings