Skip to content

Create Token-Based Group Assignment System #3

@codeiaks

Description

@codeiaks

Develop a system to automatically assign users to Discourse groups based on their token holdings (ERC20) and NFT ownership. This bridges on-chain assets with Discourse's permission system, enabling token-gated content and features within the forum.

Purpose

  • Automate group membership based on blockchain assets
  • Enable token-gated content and features in Discourse
  • Create dynamic roles that update with on-chain activity
  • Build a permission system that respects Web3 ownership without requiring gas

Technical Requirements

  • Develop a scheduler for periodic verification of token/NFT ownership
  • Create admin UI for mapping tokens/NFTs to Discourse groups
  • Implement group assignment/removal logic based on verification results
  • Add support for multiple token criteria (e.g., must own Token A AND Token B)
  • Create audit log for group assignment changes
  • Develop cache mechanism to reduce blockchain API calls
  • Add manual override options for administrators

Implementation Details

The implementation will need to:

  1. Store token/NFT → group mappings in site settings
  2. Periodically verify user ownership of configured tokens/NFTs
  3. Automatically add/remove users from groups based on verification results
  4. Support complex rule configurations (AND/OR logic, minimum amounts)
  5. Provide admin dashboard for monitoring assignments
  6. Handle expired verifications gracefully

Example Code Snippet

# Server-side scheduled job for group assignment (in plugin.rb)
module ::DiscourseEthereum
  class TokenGroupAssigner < ::Jobs::Scheduled
    every 1.day
    
    def execute(args)
      return unless SiteSetting.discourse_ethereum_group_assignment_enabled
      
      # Get configured token→group mappings from settings
      token_mappings = parse_token_mappings
      
      # Process each user with a connected wallet
      User.joins("INNER JOIN user_custom_fields ucf ON ucf.user_id = users.id")
          .where("ucf.name = 'ethereum_address' AND ucf.value IS NOT NULL")
          .find_each do |user|
          
        wallet_address = user.custom_fields["ethereum_address"]
        
        # Process each token mapping
        token_mappings.each do |mapping|
          group = Group.find_by(name: mapping[:group_name])
          next unless group
          
          should_be_member = false
          
          case mapping[:type]
          when "erc20"
            balance = verify_token_balance(wallet_address, mapping[:contract], mapping[:min_amount])
            should_be_member = balance >= mapping[:min_amount].to_f
          when "nft"
            has_nft = verify_nft_ownership(wallet_address, mapping[:contract], mapping[:token_id])
            should_be_member = has_nft
          end
          
          # Update group membership
          if should_be_member && !group.users.include?(user)
            group.add(user) 
            log_group_change(user, group, "added", "token verification")
          elsif !should_be_member && group.users.include?(user)
            group.remove(user)
            log_group_change(user, group, "removed", "token verification")
          end
        end
      end
    end
    
    private
    
    def parse_token_mappings
      SiteSetting.discourse_ethereum_token_groups.split("|").map do |mapping_str|
        type, contract, group_name, min_amount, token_id = mapping_str.split(",")
        {
          type: type, 
          contract: contract, 
          group_name: group_name,
          min_amount: min_amount || "0",
          token_id: token_id
        }
      end
    end
    
    def verify_token_balance(address, token_contract, min_amount)
      # Implementation using an API service like Etherscan, Infura, or Alchemy
      # Return token balance as a float
    end
    
    def verify_nft_ownership(address, nft_contract, token_id)
      # Implementation to verify NFT ownership
      # Return true if owned, false otherwise
    end
    
    def log_group_change(user, group, action, reason)
      StaffActionLogger.new(Discourse.system_user).log_custom(
        "ethereum_group_#{action}",
        { user_id: user.id, group_id: group.id, reason: reason }
      )
    end
  end
end
// Client-side admin configuration component (simplified)
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";

export default class TokenGroupMapping extends Component {
  @service store;
  @tracked mappings = [];
  @tracked groups = [];
  @tracked loading = true;
  
  constructor() {
    super(...arguments);
    this.loadData();
  }
  
  async loadData() {
    this.loading = true;
    try {
      // Load groups
      const groups = await this.store.findAll('group');
      this.groups = groups.toArray();
      
      // Load existing mappings from settings
      const mappingsString = this.siteSettings.discourse_ethereum_token_groups;
      if (mappingsString) {
        this.mappings = mappingsString.split('|').map(str => {
          const [type, contract, groupName, minAmount, tokenId] = str.split(',');
          return { type, contract, groupName, minAmount, tokenId };
        });
      }
    } catch (error) {
      console.error('Error loading data:', error);
    } finally {
      this.loading = false;
    }
  }
  
  @action
  addMapping() {
    this.mappings.pushObject({
      type: 'erc20',
      contract: '',
      groupName: '',
      minAmount: '0',
      tokenId: ''
    });
  }
  
  @action
  removeMapping(index) {
    this.mappings.removeAt(index);
    this.saveMappings();
  }
  
  @action
  saveMappings() {
    const mappingString = this.mappings
      .filter(m => m.contract && m.groupName)
      .map(m => `${m.type},${m.contract},${m.groupName},${m.minAmount || '0'},${m.tokenId || ''}`)
      .join('|');
    
    this.siteSettings.discourse_ethereum_token_groups = mappingString;
  }
}

Acceptance Criteria

  • Admin can configure token/NFT to group mappings through UI
  • System automatically assigns users to groups based on token ownership
  • Group assignments update periodically without manual intervention
  • Users lose group membership when they no longer meet criteria
  • System supports both ERC20 token balances and NFT ownership criteria
  • Admin can view logs of group assignment changes
  • Performance is optimized to minimize blockchain API calls

Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions