Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

SUI Wallet Authentication
BeginnerChapter 4 of 415 min read

Character Verification

In EVE Frontier, a wallet address is not enough -- you need to know the player's Character. Characters have names, tribe affiliations, and own objects like SSUs. This chapter covers how to go from a wallet address to a verified character with tribe membership.

The Data Model

``

Wallet Address (0xabc...)

└── owns PlayerProfile object

└── contains character_id

└── points to Character object

├── name (string)

├── tribe_id (u32)

├── character_address (address)

└── owns OwnerCap objects

├── OwnerCap

└── OwnerCap

`

A wallet address owns a PlayerProfile object, which contains a reference to a Character object. The character object holds the player's name, tribe, and owns capability tokens for their in-game assets.

Querying via GraphQL

The AncientStorage dApp uses SUI GraphQL to traverse from wallet to character:

typescript
const GET_WALLET_CHARACTERS = 
  query GetWalletCharacters(
    $owner: SuiAddress!,
    $characterPlayerProfileType: String!
  ) {
    address(address: $owner) {
      objects(last: 1, filter: { type: $characterPlayerProfileType }) {
        nodes {
          contents {
            extract(path: "character_id") {
              asAddress {
                address
                asObject {
                  asMoveObject {
                    contents {
                      type { repr }
                      json
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
;

How the Query Works

  • Start from the wallet address: address(address: $owner) finds all objects owned by the wallet.
  • Filter by type: filter: { type: $characterPlayerProfileType } narrows to PlayerProfile objects.
  • Extract the character ID: extract(path: "character_id") follows the reference from the profile to the character.
  • Resolve the character object: asAddress → asObject → asMoveObject → contents retrieves the character's full data.
  • Calling the Query

    typescript
    const EVE_FRONTIER_PACKAGE_ID = '0x28b497...';
    
    async function fetchWalletCharacter(
      walletAddress: string,
    ): Promise<CharacterInfo | null> {
      const profileType = ${EVE_FRONTIER_PACKAGE_ID}::character::PlayerProfile;
    
      const data = await suiGraphQL(GET_WALLET_CHARACTERS, {
        owner: walletAddress,
        characterPlayerProfileType: profileType,
      });
    
      const node = data?.address?.objects?.nodes?.[0];
      if (!node) return null;
    
      const extract = node.contents?.extract;
      const objectId = extract?.asAddress?.address ?? '';
      const json = extract?.asAddress?.asObject?.asMoveObject?.contents?.json;
      if (!json?.metadata?.name) return null;
    
      return {
        name: json.metadata.name,
        tribeId: toInt(json.tribe_id),
        characterId: toInt(json.key?.item_id),
        characterObjectId: objectId,
        characterOwnerCapId: json.owner_cap_id ?? '',
        address: json.character_address ?? '',
      };
    }

    The CharacterInfo Shape

    typescript
    interface CharacterInfo {
      name: string;              // Player's in-game name
      tribeId: number;           // Tribe ID (u32)
      characterId: number;       // Unique character ID
      characterObjectId: string; // SUI object address of the Character
      characterOwnerCapId: string; // OwnerCap<Character> object ID
      address: string;           // Character's on-chain address
    }

    The useWalletCharacter Hook

    The AncientStorage dApp wraps the character query in a React hook:

    typescript
    export function useWalletCharacter(
      walletAddress: string | undefined,
    ): WalletCharacterState {
      const [character, setCharacter] = useState<CharacterInfo | null>(null);
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [tick, setTick] = useState(0);
    
      const refresh = useCallback(() => setTick((t) => t + 1), []);
    
      useEffect(() => {
        if (!walletAddress) {
          setCharacter(null);
          setIsLoading(false);
          setError(null);
          return;
        }
    
        let cancelled = false;
        setIsLoading(true);
        setError(null);
    
        fetchWalletCharacter(walletAddress)
          .then((info) => {
            if (cancelled) return;
            setCharacter(info);
            if (!info) setError('No EVE Frontier character found for this wallet');
          })
          .catch((err) => {
            if (cancelled) return;
            setError(err instanceof Error ? err.message : 'Failed to fetch character');
          })
          .finally(() => {
            if (!cancelled) setIsLoading(false);
          });
    
        return () => { cancelled = true; };
      }, [walletAddress, tick]);
    
      return { character, isLoading, error, refresh };
    }

    Usage in Components

    typescript
    function StorageDApp() {
      const connection = useWalletConnection();
      const { character, isLoading, error } = useWalletCharacter(
        connection.account?.address,
      );
    
      if (isLoading) return <p>Loading character...</p>;
      if (error) return <p>Error: {error}</p>;
      if (!character) return <p>No character found</p>;
    
      return (
        <div>
          <h2>{character.name}</h2>
          <p>Tribe: {character.tribeId}</p>
          <p>Character ID: {character.characterId}</p>
          {/ Pass character data to transaction builders /}
        </div>
      );
    }

    Verifying Tribe Membership

    Once you have the character info, you can check tribe membership client-side before even submitting a transaction:

    typescript
    function canAccessStorage(character: CharacterInfo, requiredTribeId: number): boolean {
      return character.tribeId === requiredTribeId;
    }
    
    // In your UI
    if (!canAccessStorage(character, REQUIRED_TRIBE_ID)) {
      return <p>Your character is not in the required tribe.</p>;
    }

    This is a UX optimization -- the Move contract will still verify tribe membership on-chain. But checking client-side avoids wasting gas on transactions that will abort.

    Finding a Character's SSUs

    To find which SSUs a character owns, query for OwnerCap objects owned by the character:

    typescript
    const GET_CHARACTER_SSUS = 
      query GetCharacterSsus(
        $characterObjectId: SuiAddress!,
        $ownerCapType: String!
      ) {
        object(address: $characterObjectId) {
          objects(filter: { type: $ownerCapType }) {
            nodes {
              asMoveObject {
                contents { json }
              }
              address
            }
          }
        }
      }
    ;
    
    async function fetchCharacterSsus(
      characterObjectId: string,
    ): Promise<OwnedSsu[]> {
      const ownerCapType =
        ${EVE_FRONTIER_PACKAGE_ID}::access::OwnerCap<${EVE_FRONTIER_PACKAGE_ID}::storage_unit::StorageUnit>;
    
      const data = await suiGraphQL(GET_CHARACTER_SSUS, {
        characterObjectId,
        ownerCapType,
      });
    
      const nodes = data?.object?.objects?.nodes ?? [];
      return nodes
        .filter(n => n.asMoveObject?.contents?.json?.authorized_object_id)
        .map(n => ({
          ssuObjectId: n.asMoveObject.contents.json.authorized_object_id,
          ownerCapId: n.address,
        }));
    }

    Each OwnerCap has an authorized_object_id field pointing to the SSU it controls. This lets you build a list of SSUs the connected player owns.

    The Full Verification Chain

    When a player connects to the AncientStorage dApp, the verification chain is:

  • Wallet connected -- useWalletConnection().isConnected is true
  • Message signed -- Server session is valid
  • Character found -- useWalletCharacter() returns a character
  • Tribe verified -- Character's tribeId matches the required tribe
  • SSU identified -- Player is at or owns a configured SSU
  • Each step narrows the set of operations available to the user:

    • Without a character: no dApp features
    • Wrong tribe: can view but not interact
    • Not the SSU owner: can share/withdraw but not authorize extensions

    Key Takeaways

    • A wallet address maps to a PlayerProfile, which references a Character object containing name, tribe, and capabilities.
    • Use SUI GraphQL with extract(path: "character_id") to traverse from wallet to character in a single query.
    • The useWalletCharacter hook encapsulates the character lookup with loading, error, and refresh states.
    • Client-side tribe verification is a UX optimization -- the Move contract is the source of truth.
    • OwnerCap` objects reveal which SSUs a character owns, enabling ownership detection.
    • The verification chain (wallet, signature, character, tribe, SSU) gates access at each level.

    Sign in to track your progress.