Avatar / Profile Image
Each .vet name can have a single image representing it. This image can be a reference to a file stored on IPFS/Arweave, a link to a website, or a reference to an NFT.
Uploads of images will be available in the future.
Storage
The data is stored in the avatar
text record within the names resolver.
The relevant interface definition is:
// Logged when a text record is changed.
event TextChanged(bytes32 indexed node, string indexed key, string value);
// Function to get a text record.
function text(bytes32 node, string calldata key) external view returns (string memory);
// Function to set a text record.
function setText(bytes32 node, string calldata key, string calldata value) external;
The supported values are:
ipfs://<ipfs hash>
ar://<arweave hash>
https://<link>
eip155:<chain id>/<contract type>/<contract address>/<token id>
Access with API
Accessing the image set by the user or a generated image is possible through a public API, which redirects to the appropriate resource:
- MainNet Endpoint:
https://vet.domains/api/avatar/{name}
- TestNet Endpoint:
https://testnet.vet.domains/api/avatar/{name}
- Examples:
Access with Node.js
const { namehash, Interface } = require('ethers')
const VET_REGISTRY_ADDRESS = "0xa9231da8BF8D10e2df3f6E03Dd5449caD600129b";
const NODE_URL = "https://node-mainnet.vechain.energy"
const nameInterface = new Interface([
'function resolver(bytes32 node) returns (address resolverAddress)',
'function text(bytes32 node, string key) returns (string avatar)'
])
async function getAvatar(name) {
const node = namehash(name);
const [{ data: resolverData, reverted: noResolver }] = await fetch(`${NODE_URL}/accounts/*`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
clauses: [
{
to: VET_REGISTRY_ADDRESS,
data: nameInterface.encodeFunctionData('resolver', [node])
}
]
})
}).then(res => res.json());
// return empty string if no resolver has been set
if (noResolver) { return ''; }
const { resolverAddress } = nameInterface.decodeFunctionResult('resolver', resolverData);
const [{ data: lookupData, reverted: noLookup }] = await fetch(`${NODE_URL}/accounts/*`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
clauses: [
{
to: resolverAddress,
data: nameInterface.encodeFunctionData('text', [node, 'avatar'])
}
]
})
}).then(res => res.json());
// return empty string if resolver does respond with error
if (noLookup) { return '; }
const { avatar } = nameInterface.decodeFunctionResult('text', lookupData);
return avatar;
}
async function main() {
const testName = 'hello.vet'
const avatar = await getAvatar(testName)
console.log(testName, 'has', avatar ? 'an avatar' : 'no avatar', avatar)
console.log('getAvatar(name) replies with', avatar)
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
Example Web-Service
Example Web-Service that looks up a name and resolves NFTs into their metadata.
Can be tested on: https://www.val.town/v/ifavo/vetDomains_avatar_lookup (opens in a new tab)
import { getAddress, Interface, namehash, toBeHex, toUtf8String, zeroPadValue } from "npm:ethers";
const VET_REGISTRY_ADDRESS = "0xa9231da8BF8D10e2df3f6E03Dd5449caD600129b";
const NODE_URL = "https://node-mainnet.vechain.energy";
const nameInterface = new Interface([
"function resolver(bytes32 node) returns (address resolverAddress)",
"function text(bytes32 node, string key) returns (string avatar)",
]);
const erc721Interface = new Interface([
"function ownerOf(uint256 tokenId) view returns (address)",
"function tokenURI(uint256 tokenId) view returns (string)",
"function uri(uint256 tokenId) view returns (string)",
]);
const erc1155Interface = new Interface([
"function balanceOf(address account, uint256 tokenId) view returns (uint256)",
"function uri(uint256 tokenId) view returns (string)",
]);
interface AvatarResult {
record: string;
avatar: string | null;
}
export default async function(req: Request): Promise<Response> {
const url = new URL(req.url);
const name = url.pathname.slice(1); // Remove leading slash
// Validate name is not empty
if (!name) {
return Response.json({
error: "Please provide a VeChain name in the path, e.g. /hello.vet",
}, { status: 400 });
}
const { record, avatar } = await getAvatar(name);
return Response.json({
name,
record,
avatar,
});
}
async function verifyNftOwnership(
contractAddress: string,
tokenId: string,
ownerAddress: string,
isErc1155: boolean,
): Promise<boolean> {
try {
const clauses = [
{
to: contractAddress,
data: isErc1155
? erc1155Interface.encodeFunctionData("balanceOf", [ownerAddress, BigInt(tokenId)])
: erc721Interface.encodeFunctionData("ownerOf", [BigInt(tokenId)]),
},
];
const [{ data, reverted }] = await fetch(`${NODE_URL}/accounts/*`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ clauses }),
}).then(res => res.json());
if (reverted) return false;
if (isErc1155) {
const balance = erc1155Interface.decodeFunctionResult("balanceOf", data)[0];
return BigInt(balance) > 0n;
} else {
const owner = erc721Interface.decodeFunctionResult("ownerOf", data)[0];
return owner.toLowerCase() === ownerAddress.toLowerCase();
}
} catch (error) {
console.error("Ownership verification error:", error);
return false;
}
}
async function parseAvatarRecord(record: string): Promise<string | null> {
try {
// Direct URL handling
if (record.startsWith("http")) return record;
if (record.startsWith("ipfs://")) return `https://ipfs.io/ipfs/${record.slice(7)}`;
if (record.startsWith("ar://")) return `https://arweave.net/${record.slice(5)}`;
// Handle NFT avatar (ENS-12)
const match = record.match(/eip155:(\d+)\/(?:erc721|erc1155):([^/]+)\/(\d+)/);
if (match) {
const [, chainId, contractAddress, tokenId] = match;
const isErc1155 = record.includes("erc1155");
if (!chainId || !contractAddress || tokenId === undefined) {
return null;
}
// Try to fetch token URI
const clauses = [
{
to: contractAddress,
data: erc721Interface.encodeFunctionData(
isErc1155 ? "uri" : "tokenURI",
[BigInt(tokenId)],
),
},
];
const [{ data, reverted }] = await fetch(`${NODE_URL}/accounts/*`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ clauses }),
}).then(res => res.json());
if (reverted) {
throw new Error("Failed to fetch tokenURI");
}
// Decode the tokenURI
let tokenUri = "";
try {
tokenUri = erc721Interface.decodeFunctionResult(
isErc1155 ? "uri" : "tokenURI",
data,
)[0];
} catch (e) {
// Some contracts return bytes that need to be converted to string
tokenUri = toUtf8String(data);
}
// Handle IPFS and Arweave URIs in the tokenURI
if (tokenUri.startsWith("ipfs://")) {
tokenUri = `https://ipfs.io/ipfs/${tokenUri.slice(7)}`;
} else if (tokenUri.startsWith("ar://")) {
tokenUri = `https://arweave.net/${tokenUri.slice(5)}`;
}
// For ERC1155, replace {id} with the actual token ID
if (isErc1155) {
tokenUri = tokenUri.replace("{id}", zeroPadValue(toBeHex(BigInt(tokenId)), 32).slice(2));
}
// Fetch metadata from tokenURI
const metadataResponse = await fetch(tokenUri);
if (!metadataResponse.ok) {
throw new Error("Failed to fetch metadata");
}
const metadata = await metadataResponse.json();
const imageUrl = metadata.image || metadata.image_url || metadata.image_data;
if (!imageUrl) {
throw new Error("No image URL in metadata");
}
// Handle IPFS and Arweave URLs in the metadata
if (imageUrl.startsWith("ipfs://")) {
return `https://ipfs.io/ipfs/${imageUrl.slice(7)}`;
}
if (imageUrl.startsWith("ar://")) {
return `https://arweave.net/${imageUrl.slice(5)}`;
}
// Handle data URLs in image field
if (imageUrl.startsWith("data:")) {
return imageUrl;
}
return imageUrl;
}
return null;
} catch (error) {
console.error("Error parsing avatar record:", error);
return null;
}
}
async function getAvatar(name: string): Promise<AvatarResult> {
const node = namehash(name);
const [{ data: resolverData, reverted: noResolver }] = await fetch(`${NODE_URL}/accounts/*`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
clauses: [
{
to: VET_REGISTRY_ADDRESS,
data: nameInterface.encodeFunctionData("resolver", [node]),
},
],
}),
}).then(res => res.json());
// return empty result if no resolver has been set
if (noResolver) {
return {
record: "",
avatar: null,
};
}
const { resolverAddress } = nameInterface.decodeFunctionResult("resolver", resolverData);
const [{ data: lookupData, reverted: noLookup }] = await fetch(`${NODE_URL}/accounts/*`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
clauses: [
{
to: resolverAddress,
data: nameInterface.encodeFunctionData("text", [node, "avatar"]),
},
],
}),
}).then(res => res.json());
// return empty result if resolver does not respond
if (noLookup) {
return {
record: "",
avatar: null,
};
}
const { avatar: record } = nameInterface.decodeFunctionResult("text", lookupData);
// Parse avatar record, including NFT avatar handling
const avatar = await parseAvatarRecord(String(record));
return {
record: String(record),
avatar,
};
}
Standard Definition
The avatar records follow the ENS defined standards: