Smart Contracts
Events

Contract Events

Every state change worth watching in Superhero emits an event. This is the cross-contract catalogue: event names, argument types, the Blake2b topic hash the middleware indexes them by, and how to read them from a call, from the middleware, or from a live subscription.

How an æternity event is encoded

A Sophia Chain.event produces a log entry with a topics array and a data field:

SlotContents
topics[0]The Blake2b-256 hash of the event constructor name, as a big integer. It is implicit, not one of the declared arguments.
topics[1..]The word-sized arguments in declaration order: address, int, bool, contract references.
dataA single string argument, if the event declares one. Empty otherwise.

Mint(address, int) produces three topics (the name hash, the account, and the value) and empty data. An indexer filters logs on topics[0] and reads the amount from topics[2].

PostWithoutTipReceived(address, string) produces two topics (the name hash and the author) and puts the title in data.

The name hash is derivable offline:

import { blake2b } from 'blakejs';   // or any Blake2b-256 implementation
 
function eventTopic(name) {
  const digest = blake2b(new TextEncoder().encode(name), undefined, 32);
  return BigInt('0x' + Buffer.from(digest).toString('hex')).toString();
}
 
eventTopic('Mint');
// '97248968993606906149864095761415446114204891017168990930824289305879066770211'
import hashlib
def event_topic(name):
    return int.from_bytes(hashlib.blake2b(name.encode(), digest_size=32).digest(), 'big')

The catalogue

Tipping v3

datatype event = PostWithoutTipReceived(address, string)
                                     // author,  title
EventTopic hash
PostWithoutTipReceived83408474105809356723083641835082858468110713825767194599189017517019994517159

FungibleTokenFull (AEX-9)

Emitted by every community token and by the ProtocolDAO token.

datatype event =
    Transfer(address, address, int)    // from, to, value
  | Allowance(address, address, int)   // owner, spender, value
  | Burn(address, int)                 // account, value
  | Mint(address, int)                 // account, value
  | Swap(address, int)                 // account, value
EventTopic hash
Transfer15485047184846566156736396069994907050875216973023180189891727495730853981167
Allowance6675387653529534352993899574763440621350730065146058041830673378811230846814
Burn59519329313588602299792785325724171247065768738621522936987157301332531057158
Mint97248968993606906149864095761415446114204891017168990930824289305879066770211
Swap87152034236714252806392310986442524876185493856990839672979346500071310270671

CommunityFactory

datatype event =
    CreateCommunity(string, DAO, TokenSale, CommunityManagement)
                 // name,  dao, sale,      community management
  | CreateCollection(string, address)
                 // collection id, creator
EventTopic hash
CreateCommunity9838227481037218045844062460739435620538960633417016110994140287779986845610
CreateCollection106451687811681430585235988806070705060981919041452525929915952793933471156443

CreateCommunity is the most useful event in the system for an indexer: one log entry gives you a new community's name and all three of its contract addresses.

AffiliationBondingCurveTokenSale

datatype event =
    Buy(int, int, int)        // total_price, affiliation_amount, total_supply
  | Sell(int, int)            // return, total_supply
  | PriceChange(int, int)     // previous_price, next_price
EventTopic hash
Buy103347481884921461187458933603797704361973189016747204637339841427224784760666
Sell23104635772480053538972224151762463181492989144154121566848232077119925570281
PriceChange3577134775049335318224940963029268892731434609492265317583808375263764302639

The traded token count is not in any of these. Get it from the transaction's call arguments or from the paired Mint / Burn event on the token contract. The total_supply in Buy and Sell is the supply before the trade.

DAO

datatype event =
    VoteAdded(DAOVote, int)            // vote contract, seq id
  | AppliedVoteSubject(DAOVote, int)   // vote contract, seq id
EventTopic hash
VoteAdded100296888931796761370418720901301074287492180349358028295003277292737336490649
AppliedVoteSubject72866934355761804726444456354278340374976688824060458404971668592903548128404

DAOVote

datatype event =
    Vote(address, vote_option, amount)         // voter, true/false, token amount
  | RevokeVote(address, vote_option, amount)
  | Withdraw(address, amount)
EventTopic hash
Vote3422073428777010606992960824915966491300503610863485918227821198825793135961
RevokeVote83222614123280176911522716292168996389183038060406855328029912685286369361264
Withdraw89825458176832345744835925400658672867758610710629241958369772151454881664788

CommunityManagement

datatype event =
    ChangeMinimumTokenThreshold(int)
  | AddModerator(address)
  | DeleteModerator(address)
  | MuteUserId(string)
  | UnmuteUserId(string)
  | SetOwner(address)
  | ChangedMetaInfo
EventTopic hash
ChangeMinimumTokenThreshold71578671910977488503419808990837217417694473612503180034663459223810642793586
AddModerator115131966880940069348371790178778275850537803171988368912071335019991121363194
DeleteModerator28432919713377478169071233854505714891370181428193334591987710567744262357475
MuteUserId51220346771549649907734624848411076224948906304928622313163133052569138244628
UnmuteUserId32619416189561909869442741682283648234239856393886722913381508002231322933440
SetOwner80172520537876076408879795471180066494402217313047416830773036978865332514568
ChangedMetaInfo10335050073377245413811954146980376891356632402160017070530938091573150368814

MuteUserId and UnmuteUserId carry their string in data, not in a topic. ChangedMetaInfo has no arguments, so it has one topic and empty data. On seeing it, re-read meta_info() to find out what changed.

AffiliationTreasury

datatype event =
    UserInvited(address, address)                  // invitee, inviter
  | RewardAdded(address, int)                      // user, added amount
  | Withdrawn(address, int)                        // user, reward amount
  | InvitationRegistered(address, address, int)    // caller, invitation_address, amount
  | InvitationRedeemed(address, address, address)  // invitee_address, inviter_address, caller
  | InvitationRevoked(address, address)            // invitation_address, inviter_address
  | RecordSaleTransaction(address, int)            // buyer, amount
EventTopic hash
UserInvited111644451749685844410136074618417092744982231416455402956868241779009497576281
RewardAdded40473986023626367572251206730832917387195974458170320818930437034380042534511
Withdrawn86610342368312941386442396018534288058023538473615206361475788933815984716359
InvitationRegistered30889086819116421264520881404320781915167317994181425690778894381176657165624
InvitationRedeemed10590405277933974134328487384997411406441716171794087792891317803724026813033
InvitationRevoked44587635821633064715474581091143067134350953963411449550976976484602513073608
RecordSaleTransaction77117783428530384567038756067996288780218798482299224958129404253908571047664

Two orderings are easy to transpose: InvitationRedeemed leads with the invitee, while InvitationRevoked leads with the invite address. The third address on InvitationRedeemed is the caller, which for a redemption is the invite address itself.

UserInvited fires only from register_invitation. An invite claimed through a code emits InvitationRedeemed instead, even though both write the same affiliation-tree entry. Watch both to capture every invite relationship.

RewardAdded fires once per credited level, so one buy under a four-deep referral chain emits four of them alongside a single RecordSaleTransaction.

Registry (governance)

datatype event =
    AddPoll(Poll, int)              // poll contract, poll id
  | Delegation(address, address)    // delegator, delegatee
  | RevokeDelegation(address)       // delegator
EventTopic hash
AddPoll69968208931283917191037310555274301179410603918657205619094461743485685844988
Delegation14580878786288789199221148111503981453451669266552314083691591254671693950172
RevokeDelegation113744386600267958484688809658370302768997764629220755764047327050672141400668

Poll (governance)

datatype event =
    Vote(address, address, int)
  | RevokeVote(address, address)
EventTopic hash
Vote3422073428777010606992960824915966491300503610863485918227821198825793135961
RevokeVote83222614123280176911522716292168996389183038060406855328029912685286369361264

AddressLink

datatype event =
    ProviderRegistered(address, string)   // provider owner, provider id
  | Link(address, string)
  | Unlink(address, string)
  | PrincipalLink(address, string)
  | PrincipalUnlink(address, string)
EventTopic hash
ProviderRegistered64729158502882831771288642490316090295255081040123544514787258821424028854588
Link24916428400870038197952107974381846470686467121874627127390813018326892251825
Unlink52744121487300572517612118090938392289474454451430582807723645180269836157157
PrincipalLink33734067034041800501097175880685404869663974342910460138405033266635271063644
PrincipalUnlink65650841556643744410284701498125626234659108343589856439166449381677919258575

Every one carries an address topic and a string in data. The Link / Unlink pair covers account links; the Principal pair covers links made against an AENS name.

The data string on a link event is provider:value, colon-joined. Split it on the first colon. Provider ids match /^[a-z]{1,10}$/, so a value may contain a colon but a provider id may not. prefaens is the provider id for a preferred AENS name.

Tipping v4

Not part of Superhero's social layer, which runs v3, but in the same contract family:

datatype event = PostViaBurnReceived(tip_id, address, string, TokenContract)
EventTopic hash
PostViaBurnReceived82774704438961085039490474074506978272280842136924042967135740228700002403902

Name collisions: filter by contract, always

The topic hash is computed from the name alone. It carries no contract identity and no argument types. Three names collide across the suite:

NameEmitted byArguments
VoteDAOVote(address, bool, int)
VotePoll(address, address, int)
RevokeVoteDAOVote(address, bool, int)
RevokeVotePoll(address, address)
AddModerator / DeleteModerator / ChangeMinimumTokenThresholdCommunityManagement eventsalso VoteSubjects.subject constructor names

A Vote log from a governance poll and a Vote log from a community DAO have the same topic hash and different argument shapes. An indexer that matches on topics[0] alone and then decodes positionally will mis-parse one of them.

Filter on the emitting contract address first, then decode. The same discipline applies within a single transaction: a token buy emits Mint twice, from two different token contracts.

Reading events from a call you made

The SDK decodes them for you against the ACI:

const result = await sale.buy(count, { amount, omitUnknown: true });
 
for (const e of result.decodedEvents) {
  console.log(e.name, e.contract.address, e.args);
}

decodedEvents covers events from every contract the call touched, so a buy gives you the sale's Buy and PriceChange plus both Mint events. Match on e.contract.address to tell the community token's mint from the ProtocolDAO token's:

const tokenMint = result.decodedEvents.find(
  (e) => e.name === 'Mint' && e.contract.address === tokenAddress,
);

Reading events from the middleware

For transactions you did not submit, query the middleware's contract log index:

GET {middlewareUrl}/v3/contracts/logs?contract_id={ct_...}&tx_hash={th_...}&limit=100
NetworkMiddleware base URL
Mainnethttps://mainnet.aeternity.io/mdw
Testnethttps://testnet.aeternity.io/mdw

Drop tx_hash to get a contract's full log history, paged. Each entry carries topics as an array of big-integer strings and data as the string payload, so matching is a string compare against the hashes above:

const MINT_TOPIC = '97248968993606906149864095761415446114204891017168990930824289305879066770211';
 
const res = await fetch(
  `https://mainnet.aeternity.io/mdw/v3/contracts/logs?contract_id=${saleAddress}&limit=100`,
);
const { data } = await res.json();
 
const mints = data.filter((log) => log.topics?.[0] === MINT_TOPIC);
for (const log of mints) {
  console.log('account topic:', log.topics[1], 'value:', log.topics[2]);
}

A log entry also carries event_hash and, when the middleware can resolve the contract's ACI, a decoded event_name and an args array:

const eventHash = log.event_hash;
const eventName = log.event_name;       // may be absent
const addressTopic = log.args?.[0];     // decoded topic arguments
const payload = log.data;

event_hash is the same 32-byte Blake2b-256 digest as topics[0], encoded as padded base32hex (RFC 4648's extended-hex alphabet, not standard base32). Match on event_hash when you want a stable identifier, and on event_name only as a convenience, since it is present only when the middleware has the contract's ACI.

Two operational notes:

  • Paginate. The response carries a next field with a relative path. Follow it until it is absent; do not assume one page is the whole history.
  • Logs lag the transaction. A transaction hash is available the moment it is mined, but the middleware may not have indexed its logs yet, so a tx_hash-filtered query can return an empty list for a transaction that emitted events. Retry, or reconcile in a later catch-up pass.

Live subscriptions

The middleware exposes a WebSocket interface for subscribing to chain activity, so an indexer does not have to poll /v3/contracts/logs.

NetworkWebSocket URL
Mainnetwss://mainnet.aeternity.io/mdw/v2/websocket
Testnetwss://testnet.aeternity.io/mdw/v2/websocket

The subscribe message is a JSON object with up to four fields:

{ "op": "Subscribe", "payload": "Transactions", "source": "mdw" }
FieldValues
opSubscribe or Unsubscribe
payloadthe channel: Transactions, MicroBlocks, KeyBlocks, or Object
targetoptional; the object to watch, for the Object channel
sourceoptional; mdw or node

Unsubscribing sends the identical object with op flipped to Unsubscribe. Reconnect on a timeout: a dropped socket is silent, and the gap is otherwise invisible.

The Superhero API republishes decoded token activity on its own WebSocket, which is usually the easier integration if you only care about tokens. It emits two channels, token-updated and token-history. See WebSocket API.

Related