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
Implementation Details
The implementation will need to:
- Store token/NFT → group mappings in site settings
- Periodically verify user ownership of configured tokens/NFTs
- Automatically add/remove users from groups based on verification results
- Support complex rule configurations (AND/OR logic, minimum amounts)
- Provide admin dashboard for monitoring assignments
- 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
Resources
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
Technical Requirements
Implementation Details
The implementation will need to:
Example Code Snippet
Acceptance Criteria
Resources