Skip to main content

Overview

The ZKScore Identity SBT contract emits events for all significant state changes. These events are essential for tracking identity lifecycle, monitoring contract activity, and building off-chain applications.
Events are stored in the blockchain and can be queried by indexers. They provide a reliable way to track contract state changes and build real-time applications.

Standard ERC-721 Events

Transfer

Emitted when a token is transferred from one address to another.
event Transfer(
    address indexed from,
    address indexed to,
    uint256 indexed tokenId
);
Parameters:
  • from (address indexed): The previous owner of the token
  • to (address indexed): The new owner of the token
  • tokenId (uint256 indexed): The ID of the token
When Emitted:
  • When a token is minted (from is zero address)
  • When a token is transferred between addresses
  • When a token is burned (to is zero address)
Note: This event is not emitted for activated (soulbound) tokens since they cannot be transferred. Example Usage:
// Listen for all transfer events
contract.on('Transfer', (from, to, tokenId, event) => {
  console.log(`Token ${tokenId} transferred from ${from} to ${to}`);
});

// Filter for specific transfers
const filter = contract.filters.Transfer(null, '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
contract.on(filter, (from, to, tokenId, event) => {
  console.log(`Token ${tokenId} transferred to alice`);
});

// Get historical transfers
async function getTransfers(tokenId) {
  const filter = contract.filters.Transfer(null, null, tokenId);
  const events = await contract.queryFilter(filter);
  return events;
}

Approval

Emitted when an address is approved to transfer a specific token.
event Approval(
    address indexed owner,
    address indexed approved,
    uint256 indexed tokenId
);
Parameters:
  • owner (address indexed): The owner of the token
  • approved (address indexed): The approved address
  • tokenId (uint256 indexed): The ID of the token
When Emitted:
  • When approve() is called
  • When approval is revoked (approved address is zero)
Note: This event is not emitted for activated (soulbound) tokens since they cannot be transferred.

ApprovalForAll

Emitted when an operator is approved or revoked for all tokens.
event ApprovalForAll(
    address indexed owner,
    address indexed operator,
    bool approved
);
Parameters:
  • owner (address indexed): The owner of the tokens
  • operator (address indexed): The operator address
  • approved (bool): True if approved, false if revoked
When Emitted:
  • When setApprovalForAll() is called

Soulbound Token Events

IdentityMinted

Emitted when a new identity token is minted.
event IdentityMinted(
    address indexed to,
    uint256 indexed tokenId,
    string name
);
Parameters:
  • to (address indexed): The address the token was minted to
  • tokenId (uint256 indexed): The ID of the minted token
  • name (string): The ZKS ID name
When Emitted:
  • When mint() is called successfully
Example Usage:
// Listen for identity minting events
contract.on('IdentityMinted', (to, tokenId, name, event) => {
  console.log(`New identity minted: ${name} (ID: ${tokenId}) to ${to}`);
});

// Filter for specific user
const filter = contract.filters.IdentityMinted('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
contract.on(filter, (to, tokenId, name, event) => {
  console.log(`Alice's identity minted: ${name} (ID: ${tokenId})`);
});

// Get all minted identities
async function getAllMintedIdentities() {
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter);
  return events.map(event => ({
    to: event.args.to,
    tokenId: event.args.tokenId,
    name: event.args.name,
    blockNumber: event.blockNumber,
    transactionHash: event.transactionHash
  }));
}

IdentityActivated

Emitted when an identity token is activated (made soulbound).
event IdentityActivated(
    uint256 indexed tokenId,
    address indexed owner
);
Parameters:
  • tokenId (uint256 indexed): The ID of the activated token
  • owner (address indexed): The owner of the token
When Emitted:
  • When activate() is called successfully
Example Usage:
// Listen for activation events
contract.on('IdentityActivated', (tokenId, owner, event) => {
  console.log(`Identity ${tokenId} activated by ${owner}`);
});

// Filter for specific token
const filter = contract.filters.IdentityActivated(123);
contract.on(filter, (tokenId, owner, event) => {
  console.log(`Token 123 activated by ${owner}`);
});

// Get activation history
async function getActivationHistory(tokenId) {
  const filter = contract.filters.IdentityActivated(tokenId);
  const events = await contract.queryFilter(filter);
  return events;
}

MetadataUpdated

Emitted when a token’s metadata URI is updated.
event MetadataUpdated(
    uint256 indexed tokenId,
    string newURI
);
Parameters:
  • tokenId (uint256 indexed): The ID of the token
  • newURI (string): The new metadata URI
When Emitted:
  • When setTokenURI() is called successfully
Note: This event is not emitted for activated tokens since their metadata is immutable.

SoulboundStatusChanged

Emitted when a token’s soulbound status changes.
event SoulboundStatusChanged(
    uint256 indexed tokenId,
    bool isSoulbound
);
Parameters:
  • tokenId (uint256 indexed): The ID of the token
  • isSoulbound (bool): True if the token is now soulbound, false otherwise
When Emitted:
  • When activate() is called (isSoulbound becomes true)
  • When soulbound status is changed by admin (rare)

Event Indexing

Indexed Parameters

Events have up to 3 indexed parameters that can be filtered efficiently:
event IdentityMinted(
    address indexed to,        // Indexed - can filter by address
    uint256 indexed tokenId,   // Indexed - can filter by token ID
    string name                // Not indexed - cannot filter efficiently
);

Filtering Examples

// Filter by indexed parameters
const filter1 = contract.filters.IdentityMinted('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
const filter2 = contract.filters.IdentityMinted(null, 123);
const filter3 = contract.filters.IdentityMinted('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', 123);

// Filter by block range
async function getEventsInRange(startBlock, endBlock) {
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter, startBlock, endBlock);
  return events;
}

// Filter by multiple criteria
async function getRecentMints(limit = 10) {
  const currentBlock = await provider.getBlockNumber();
  const startBlock = currentBlock - 1000; // Last 1000 blocks
  
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter, startBlock, currentBlock);
  
  return events.slice(-limit); // Last 10 events
}

Event Monitoring

Real-time Monitoring

// Set up real-time event monitoring
class IdentityEventMonitor {
  constructor(contract) {
    this.contract = contract;
    this.listeners = new Map();
  }
  
  startMonitoring() {
    // Monitor all identity events
    this.contract.on('IdentityMinted', (to, tokenId, name, event) => {
      this.handleEvent('IdentityMinted', {
        to,
        tokenId,
        name,
        blockNumber: event.blockNumber,
        transactionHash: event.transactionHash
      });
    });
    
    this.contract.on('IdentityActivated', (tokenId, owner, event) => {
      this.handleEvent('IdentityActivated', {
        tokenId,
        owner,
        blockNumber: event.blockNumber,
        transactionHash: event.transactionHash
      });
    });
  }
  
  handleEvent(eventType, data) {
    console.log(`Event: ${eventType}`, data);
    
    // Notify listeners
    if (this.listeners.has(eventType)) {
      this.listeners.get(eventType).forEach(callback => {
        callback(data);
      });
    }
  }
  
  on(eventType, callback) {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType).push(callback);
  }
  
  off(eventType, callback) {
    if (this.listeners.has(eventType)) {
      const callbacks = this.listeners.get(eventType);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }
}

// Usage
const monitor = new IdentityEventMonitor(contract);
monitor.startMonitoring();

monitor.on('IdentityMinted', (data) => {
  console.log('New identity minted:', data);
});

monitor.on('IdentityActivated', (data) => {
  console.log('Identity activated:', data);
});

Historical Event Queries

// Get all events for a specific token
async function getTokenEvents(tokenId) {
  const events = {
    minted: await contract.queryFilter(contract.filters.IdentityMinted(null, tokenId)),
    activated: await contract.queryFilter(contract.filters.IdentityActivated(tokenId)),
    transfers: await contract.queryFilter(contract.filters.Transfer(null, null, tokenId))
  };
  
  return events;
}

// Get events by block range
async function getEventsByBlockRange(startBlock, endBlock) {
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter, startBlock, endBlock);
  
  return events.map(event => ({
    type: 'IdentityMinted',
    to: event.args.to,
    tokenId: event.args.tokenId,
    name: event.args.name,
    blockNumber: event.blockNumber,
    transactionHash: event.transactionHash
  }));
}

// Get recent events
async function getRecentEvents(limit = 100) {
  const currentBlock = await provider.getBlockNumber();
  const startBlock = currentBlock - 10000; // Last 10,000 blocks
  
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter, startBlock, currentBlock);
  
  return events.slice(-limit);
}

Event Analytics

Event Statistics

// Analyze event patterns
async function analyzeEvents() {
  const currentBlock = await provider.getBlockNumber();
  const startBlock = currentBlock - 100000; // Last 100,000 blocks
  
  const filter = contract.filters.IdentityMinted();
  const events = await contract.queryFilter(filter, startBlock, currentBlock);
  
  const stats = {
    totalMints: events.length,
    uniqueUsers: new Set(events.map(e => e.args.to)).size,
    averageMintsPerBlock: events.length / 100000,
    mostActiveUser: getMostActiveUser(events),
    mintingTrend: getMintingTrend(events)
  };
  
  return stats;
}

function getMostActiveUser(events) {
  const userCounts = {};
  events.forEach(event => {
    const user = event.args.to;
    userCounts[user] = (userCounts[user] || 0) + 1;
  });
  
  return Object.entries(userCounts)
    .sort(([,a], [,b]) => b - a)[0];
}

function getMintingTrend(events) {
  const blockCounts = {};
  events.forEach(event => {
    const block = event.blockNumber;
    blockCounts[block] = (blockCounts[block] || 0) + 1;
  });
  
  return Object.entries(blockCounts)
    .sort(([a], [b]) => a - b)
    .map(([block, count]) => ({ block: parseInt(block), count }));
}

Best Practices

Event Handling

  1. Always handle errors: Event listeners can fail, implement proper error handling
  2. Use filters efficiently: Filter events by indexed parameters when possible
  3. Monitor gas costs: Event queries can be expensive for large ranges
  4. Implement rate limiting: Don’t overwhelm your application with too many events
  5. Store event data: Consider storing important event data in a database

Performance Optimization

  1. Use indexed parameters: Filter by indexed parameters for better performance
  2. Limit block ranges: Query smaller block ranges to avoid timeouts
  3. Cache results: Cache frequently accessed event data
  4. Use pagination: Implement pagination for large event datasets
  5. Monitor memory usage: Large event queries can consume significant memory