Skip to content

Integration Example

Complete example of integrating ERC-7866 into a dApp.

Gaming Example: Character Profiles

A game that uses ERC-7866 for player identities.

Setup

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { IERC7866 } from "../interfaces/IERC7866.sol";

contract RPGGame {
    IERC7866 public soulProfiles;

    struct GameCharacter {
        address owner;
        string class;
        uint256 level;
        uint256 experience;
    }

    mapping(address => GameCharacter) public characters;

    event CharacterCreated(address indexed player, string class);
    event CharacterLeveledUp(address indexed player, uint256 newLevel);

    constructor(address _soulProfileAddress) {
        soulProfiles = IERC7866(_soulProfileAddress);
    }

    // Game-specific functions...
}

Display Player Profile

solidity
function getPlayerInfo(address player)
    external
    view
    returns (
        string memory username,
        string memory displayName,
        string memory avatar,
        string memory characterClass,
        uint256 level
    )
{
    // Default values
    username = "Anonymous";
    avatar = "";
    displayName = "Player";

    // Fetch from ERC-7866 if profile exists
    if (soulProfiles.hasProfile(player)) {
        IERC7866.Profile memory profile = soulProfiles.getProfile(player);
        username = profile.username;

        // Try to get game-specific avatar
        IERC7866.DappAvatar memory gameAvatar = soulProfiles.getDappAvatar(
            player,
            "RPGGame"
        );

        if (bytes(gameAvatar.avatarURI).length > 0) {
            avatar = gameAvatar.avatarURI;
        } else if (bytes(profile.defaultAvatarURI).length > 0) {
            avatar = profile.defaultAvatarURI;
        }

        displayName = username;
    }

    GameCharacter memory character = characters[player];

    return (
        username,
        displayName,
        avatar,
        character.class,
        character.level
    );
}

Create Game Character

solidity
function createCharacter(string memory characterClass) external {
    require(characters[msg.sender].owner == address(0), "Character already exists");
    require(
        keccak256(abi.encodePacked(characterClass)) == keccak256(abi.encodePacked("Warrior"))
            || keccak256(abi.encodePacked(characterClass)) == keccak256(abi.encodePacked("Mage"))
            || keccak256(abi.encodePacked(characterClass)) == keccak256(abi.encodePacked("Rogue")),
        "Invalid class"
    );

    characters[msg.sender] = GameCharacter({
        owner: msg.sender,
        class: characterClass,
        level: 1,
        experience: 0
    });

    emit CharacterCreated(msg.sender, characterClass);
}

Leaderboard with Profiles

solidity
function getLeaderboard(uint256 limit)
    external
    view
    returns (
        address[] memory players,
        string[] memory usernames,
        uint256[] memory levels,
        string[] memory avatars
    )
{
    // In a real implementation, you'd maintain a proper leaderboard
    // This is a simplified version

    players = new address[](limit);
    usernames = new string[](limit);
    levels = new uint256[](limit);
    avatars = new string[](limit);

    // Fetch top players by level
    // For brevity, showing the structure only

    return (players, usernames, levels, avatars);
}

Frontend Integration

TypeScript/Web3.js Example

typescript
import { ethers } from "ethers";

interface PlayerProfile {
    address: string;
    username: string;
    avatar: string;
    class: string;
    level: number;
}

class GameIntegration {
    private game: ethers.Contract;
    private soulProfiles: ethers.Contract;
    private provider: ethers.Provider;

    constructor(
        gameAddress: string,
        soulProfileAddress: string,
        rpc: string
    ) {
        this.provider = new ethers.JsonRpcProvider(rpc);
        this.game = new ethers.Contract(gameAddress, GAME_ABI, this.provider);
        this.soulProfiles = new ethers.Contract(
            soulProfileAddress,
            ERC7866_ABI,
            this.provider
        );
    }

    async getPlayerProfile(playerAddress: string): Promise<PlayerProfile> {
        const [info] = await this.game.getPlayerInfo(playerAddress);

        return {
            address: playerAddress,
            username: info.username,
            avatar: info.avatar,
            class: info.characterClass,
            level: Number(info.level)
        };
    }

    async createCharacter(characterClass: string): Promise<void> {
        const signer = this.provider.getSigner();
        const gameWithSigner = this.game.connect(signer);

        const tx = await gameWithSigner.createCharacter(characterClass);
        await tx.wait();
    }

    async getLeaderboard(limit: number = 10): Promise<PlayerProfile[]> {
        const [players, usernames, levels, avatars] = await this.game.getLeaderboard(limit);

        return players.map((player: string, i: number) => ({
            address: player,
            username: usernames[i],
            avatar: avatars[i],
            class: "",
            level: Number(levels[i])
        }));
    }

    async listenToProfileChanges(playerAddress: string): Promise<void> {
        this.soulProfiles.on(
            "DefaultAvatarUpdated",
            (owner: string, avatarURI: string) => {
                if (owner.toLowerCase() === playerAddress.toLowerCase()) {
                    console.log(`Player avatar updated to ${avatarURI}`);
                    // Refresh UI
                }
            }
        );
    }
}

// Usage
const integration = new GameIntegration(
    "0xGameAddress",
    "0xSoulProfileAddress",
    "https://rpc.example.com"
);

const profile = await integration.getPlayerProfile("0xPlayerAddress");
console.log(profile);

await integration.createCharacter("Warrior");

const leaderboard = await integration.getLeaderboard(10);
console.log(leaderboard);

React Component Example

tsx
import React, { useEffect, useState } from "react";
import { useContractRead } from "wagmi";

interface PlayerCardProps {
    playerAddress: `0x${string}`;
}

export function PlayerCard({ playerAddress }: PlayerCardProps) {
    const [profile, setProfile] = useState<PlayerProfile | null>(null);
    const [loading, setLoading] = useState(true);

    const { data: playerInfo } = useContractRead({
        address: GAME_ADDRESS,
        abi: GAME_ABI,
        functionName: "getPlayerInfo",
        args: [playerAddress],
        watch: true
    });

    useEffect(() => {
        if (playerInfo) {
            setProfile({
                address: playerAddress,
                username: playerInfo[0],
                avatar: playerInfo[2],
                class: playerInfo[3],
                level: Number(playerInfo[4])
            });
            setLoading(false);
        }
    }, [playerInfo]);

    if (loading) return <div>Loading...</div>;
    if (!profile) return <div>Profile not found</div>;

    return (
        <div className="player-card">
            <img
                src={profile.avatar || "/default-avatar.png"}
                alt={profile.username}
                className="player-avatar"
            />
            <h2>{profile.username}</h2>
            <p className="class">Class: {profile.class}</p>
            <p className="level">Level: {profile.level}</p>
            <a href={`/player/${profile.address}`}>View Profile</a>
        </div>
    );
}

export function Leaderboard() {
    const [players, setPlayers] = useState<PlayerProfile[]>([]);

    const { data: leaderboard } = useContractRead({
        address: GAME_ADDRESS,
        abi: GAME_ABI,
        functionName: "getLeaderboard",
        args: [10],
        watch: true
    });

    useEffect(() => {
        if (leaderboard) {
            const [addresses, usernames, levels, avatars] = leaderboard;
            const formatted = addresses.map((addr: string, i: number) => ({
                address: addr,
                username: usernames[i],
                avatar: avatars[i],
                level: Number(levels[i]),
                class: ""
            }));
            setPlayers(formatted);
        }
    }, [leaderboard]);

    return (
        <div className="leaderboard">
            <h1>Top Players</h1>
            <table>
                <thead>
                    <tr>
                        <th>Rank</th>
                        <th>Player</th>
                        <th>Level</th>
                        <th>Avatar</th>
                    </tr>
                </thead>
                <tbody>
                    {players.map((player, idx) => (
                        <tr key={player.address}>
                            <td>{idx + 1}</td>
                            <td>
                                <a href={`/player/${player.address}`}>
                                    {player.username}
                                </a>
                            </td>
                            <td>{player.level}</td>
                            <td>
                                <img src={player.avatar} alt={player.username} />
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
}

Event Listening & Indexing

Listen to Profile Updates

typescript
const contract = new ethers.Contract(
    SOUL_PROFILE_ADDRESS,
    ERC7866_ABI,
    provider
);

// Listen for new profiles
contract.on("ProfileCreated", (owner, username) => {
    console.log(`New profile: ${username} by ${owner}`);

    // Update database
    db.profiles.upsert({
        address: owner,
        username: username,
        createdAt: new Date()
    });
});

// Listen for avatar updates
contract.on("DefaultAvatarUpdated", (owner, avatarURI) => {
    console.log(`${owner} updated avatar`);

    // Invalidate cache
    cache.invalidate(`avatar:${owner}`);
});

// Listen for dApp-specific avatars
contract.on(
    "DappAvatarSet",
    (owner, dappName, avatarURI, isPublic) => {
        console.log(
            `${owner} set ${dappName} avatar (public: ${isPublic})`
        );

        // Update game-specific avatar
        db.gameAvatars.upsert({
            owner,
            dappName,
            avatarURI,
            isPublic
        });
    }
);

Build Player Index

typescript
async function buildPlayerIndex() {
    const profiles = new Map<string, PlayerProfile>();

    const filter = contract.filters.ProfileCreated();
    const events = await contract.queryFilter(filter, 0, "latest");

    for (const event of events) {
        const [owner, username] = event.args;
        profiles.set(username, {
            address: owner,
            username: username,
            avatar: await contract.getDefaultAvatar(owner),
            class: "",
            level: 0
        });
    }

    return profiles;
}

Error Handling

solidity
function safeGetPlayerProfile(address player)
    external
    view
    returns (
        bool exists,
        string memory username,
        string memory avatar
    )
{
    try soulProfiles.getProfile(player) returns (
        IERC7866.Profile memory profile
    ) {
        return (
            true,
            profile.username,
            profile.defaultAvatarURI
        );
    } catch {
        return (false, "", "");
    }
}

Production Checklist

  • [ ] Test profile creation flow
  • [ ] Test private avatar access control
  • [ ] Verify fallback to default avatar works
  • [ ] Test avatar updates don't break game
  • [ ] Implement leaderboard caching
  • [ ] Set up event indexing
  • [ ] Handle non-existent profiles gracefully
  • [ ] Validate avatar URIs (no malicious content)
  • [ ] Test cross-chain deployments
  • [ ] Benchmark gas costs
  • [ ] Document for frontend team

Released under the MIT License.