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:
typescriptconst 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
typescriptconst 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
typescriptinterface 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:
typescriptexport 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
typescriptfunction 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:
typescriptfunction 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:
typescriptconst 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.
OwnerCapSign in to track your progress.