Skip to content

Commit

Permalink
add nft generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Wilbert957 committed Jan 14, 2025
1 parent f5e7fd8 commit 1bca358
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 34 deletions.
3 changes: 2 additions & 1 deletion agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"start": "node --loader ts-node/esm src/index.ts",
"generate-nft": "node --loader ts-node/esm src/generateNFT.ts",
"dev": "node --loader ts-node/esm src/index.ts",
"check-types": "tsc --noEmit",
"test": "jest"
Expand Down Expand Up @@ -66,7 +67,7 @@
"yargs": "17.7.2",
"@typechain/ethers-v6": "^0.5.0",
"ethers": "6.13.1",
"@0glabs/0g-ts-sdk": "0.2.1"
"@0glabs/0g-ts-sdk": "0.2.3"
},
"devDependencies": {
"@types/jest": "^29.5.14",
Expand Down
13 changes: 9 additions & 4 deletions agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export function parseArguments(): ParsedArguments {
type: "string",
description: "Comma separated list of NFT token IDs to load configuration from",
})
.option("dir", {
type: "string",
description: "Base directory storing the agent data",
default: ""
})
.parseSync();
} catch (error) {
elizaLogger.error("Error parsing arguments:", error);
Expand Down Expand Up @@ -609,15 +614,15 @@ export function checkPortAvailable(port: number): Promise<boolean> {
});
}

export async function loadFromNFT(tokenId: string): Promise<Character[]> {
const agentNFTClient = new AgentNFTClient("./data");
export async function loadFromNFT(tokenId: string, baseDir: string = ""): Promise<Character[]> {
const agentNFTClient = new AgentNFTClient(baseDir);

const name = await agentNFTClient.getNFTName();
elizaLogger.info(`NFT name: ${name}`);
const symbol = await agentNFTClient.getNFTSymbol();
elizaLogger.info(`NFT symbol: ${symbol}`);
const { chainURL, indexerURL } = await agentNFTClient.getTokenURI(tokenId);
elizaLogger.info(`Chain URL: ${chainURL}`);
const { rpcURL, indexerURL } = await agentNFTClient.getTokenURI(tokenId);
elizaLogger.info(`Rpc URL: ${rpcURL}`);
elizaLogger.info(`Indexer URL: ${indexerURL}`);


Expand Down
171 changes: 145 additions & 26 deletions agent/src/agentNFTClient.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { ethers } from 'ethers';
import { ethers, toUtf8Bytes } from 'ethers';
import fs from 'fs';
import path from 'path';
import { elizaLogger, stringToUuid } from "@elizaos/core";
import { TokenData, AgentMetadata } from './types';
import { AgentNFT } from './contracts/AgentNFT';
import { AgentNFT__factory } from './contracts/factories/AgentNFT__factory';
import { Indexer } from '@0glabs/0g-ts-sdk';
import { Indexer, ZgFile } from '@0glabs/0g-ts-sdk';

const NUM_AGENT_HASHES = 2;

export class AgentNFTClient {
private provider: ethers.Provider;
private signer?: ethers.Signer;
private signer?: ethers.Wallet;
private contract: AgentNFT;
private baseDir: string;
private chainURL: string;
private rpcURL: string;
private indexerURL: string;

constructor(baseDir: string = "./data") {
elizaLogger.info("AgentNFTClient constructor");
constructor(baseDir: string = "") {
this.baseDir = baseDir;

const rpcUrl = process.env.ZEROG_RPC_URL;
this.rpcURL = process.env.ZEROG_RPC_URL;
this.indexerURL = process.env.ZEROG_INDEXER_RPC_URL;

const privateKey = process.env.ZEROG_PRIVATE_KEY;
const contractAddress = process.env.ZEROG_NFT_CONTRACT_ADDRESS;

if (!rpcUrl || !contractAddress || !privateKey) {
throw new Error("Missing required environment variables: CHAIN_RPC_URL or NFT_CONTRACT_ADDRESS or PRIVATE_KEY");
if (!this.rpcURL || !contractAddress || !privateKey || !this.indexerURL) {
throw new Error("Missing required environment variables: CHAIN_RPC_URL or NFT_CONTRACT_ADDRESS or PRIVATE_KEY or INDEXER_RPC_URL");
}
try {
this.provider = new ethers.JsonRpcProvider(rpcUrl);
this.provider = new ethers.JsonRpcProvider(this.rpcURL);
this.signer = new ethers.Wallet(privateKey, this.provider);
this.contract = AgentNFT__factory.connect(
contractAddress,
Expand All @@ -42,21 +43,36 @@ export class AgentNFTClient {
}

async getNFTName(): Promise<string> {
const name = await this.contract.name();
return name;
try {
const name = await this.contract.name();
return name;
} catch (error) {
elizaLogger.error(`Failed to get NFT name:`, error);
throw error;
}
}

async getNFTSymbol(): Promise<string> {
const symbol = await this.contract.symbol();
return symbol;
try {
const symbol = await this.contract.symbol();
return symbol;
} catch (error) {
elizaLogger.error(`Failed to get NFT symbol:`, error);
throw error;
}
}

async getTokenURI(tokenId: string): Promise<{ chainURL: string, indexerURL: string }> {
const uri = await this.contract.tokenURI(tokenId);
let [chainURL, indexerURL] = uri.split("\n");
this.chainURL = chainURL.replace("chainURL: ", "");
this.indexerURL = indexerURL.replace("indexerURL: ", "");
return { chainURL, indexerURL };
async getTokenURI(tokenId: string): Promise<{ rpcURL: string, indexerURL: string }> {
try {
const uri = await this.contract.tokenURI(tokenId);
let [rpcURL, indexerURL] = uri.split("\n");
this.rpcURL = rpcURL.replace("rpcURL: ", "");
this.indexerURL = indexerURL.replace("indexerURL: ", "");
return { rpcURL, indexerURL };
} catch (error) {
elizaLogger.error(`Failed to get token URI for token ${tokenId}:`, error);
throw error;
}
}

async getTokenData(tokenId: string): Promise<TokenData> {
Expand All @@ -81,6 +97,32 @@ export class AgentNFTClient {
}
}

async mintToken(proofs: string[], dataDescriptions: string[]): Promise<string> {
try {
const tx = await this.contract.mint(proofs, dataDescriptions);
const receipt = await tx.wait();
const mintEvent = receipt?.logs
.map(log => {
try {
return this.contract.interface.parseLog(log);
} catch (e) {
return null;
}
})
.find(event => event?.name === 'Minted');

if (!mintEvent) {
throw new Error('Minted event not found in transaction receipt');
}

const tokenId = mintEvent.args[0];
return tokenId.toString();
} catch (error) {
elizaLogger.error(`Failed to mint token:`, error);
throw error;
}
}

async validateToken(tokenData: TokenData): Promise<boolean> {
try {
const tokenOwner = tokenData.owner.toLowerCase();
Expand All @@ -99,20 +141,24 @@ export class AgentNFTClient {
}

async downloadAndSaveData(tokenId: string, dataHashes: string[], dataDescriptions: string[]): Promise<AgentMetadata> {
const tokenDir = path.join(this.baseDir, stringToUuid(tokenId));
if (this.baseDir === "") {
this.baseDir = path.join("./data", stringToUuid(tokenId));
}
const agentMetadata: AgentMetadata = {
character: path.join(tokenDir, "character.json"),
memory: path.join(tokenDir, "database.sqlite")
character: path.join(this.baseDir, "character.json"),
memory: path.join(this.baseDir, "database.sqlite")
};

if (!fs.existsSync(tokenDir)) {
fs.mkdirSync(tokenDir, { recursive: true });
if (!fs.existsSync(this.baseDir)) {
fs.mkdirSync(this.baseDir, { recursive: true });
}

if (dataHashes.length !== NUM_AGENT_HASHES || dataDescriptions.length !== NUM_AGENT_HASHES) {
throw new Error(`Expected ${NUM_AGENT_HASHES} data hashes and descriptions, got ${dataHashes.length} hashes and ${dataDescriptions.length} descriptions`);
}

elizaLogger.info(`Downloading data for token ${tokenId}`);

try {
// download data from 0G storage network
for (const [hash, description] of dataHashes.map((hash, index) => [hash, dataDescriptions[index]])) {
Expand All @@ -138,8 +184,81 @@ export class AgentNFTClient {
if (err !== null) {
elizaLogger.error(`Error indexer downloading file: ${err.message}`);
}
elizaLogger.info(`File downloaded successfully to ${filePath}`);
} catch (err) {
elizaLogger.error(`Error fetching file: ${err.message}`);
}
}
}

private async uploadData(filePath: string): Promise<{ tx: string, root: string }> {
try{
elizaLogger.info(`Uploading data to indexer ${this.indexerURL}`);
const indexer = new Indexer(this.indexerURL);
if (!process.env.ZEROG_PRIVATE_KEY) {
throw new Error("Missing required environment variables: ZEROG_PRIVATE_KEY");
}

const file = await ZgFile.fromFilePath(filePath);
var [tree, err] = await file.merkleTree();
var root = tree.rootHash();
if (err === null) {
elizaLogger.info("Data root hash:", root);
} else {
elizaLogger.error("Error generating data root hash");
}
var [tx, err] = await indexer.upload(file, this.rpcURL, this.signer);
if (err !== null) {
if (err.message.includes("Data already exists")) {
elizaLogger.info("Data already exists in storage network, skipping upload");
} else {
elizaLogger.error(`Error indexer uploading file: ${err.message}`);
}
}
elizaLogger.info("Data uploaded to storage network successfully");
return { tx, root };
} catch (error) {
elizaLogger.error(`Error uploading data ${filePath} to storage network: ${error.message}`);
}
}

private async generateOwnershipProof(preimages: string[], claimedHashes: string[]): Promise<string[]> {

// TODO: generate proof using preimage and claimedHash, now just return the claimedHash as public input
return claimedHashes;
}

async generateAgentNFT(): Promise<string> {
try {
if (this.baseDir === "") {
elizaLogger.error("Base directory not set");
throw new Error("Base directory not set");
}

const agentMetadata: AgentMetadata = {
character: path.join(this.baseDir, "character.json"),
memory: path.join(this.baseDir, "database.sqlite")
};

if (!fs.existsSync(agentMetadata.character) || !fs.existsSync(agentMetadata.memory)) {
elizaLogger.error("Agent metadata files do not exist");
throw new Error("Agent metadata files do not exist");
}

// upload data to storage network
const { tx: _characterTx, root: characterRoot } = await this.uploadData(agentMetadata.character);
const { tx: _memoryTx, root: memoryRoot } = await this.uploadData(agentMetadata.memory);

// generate ownership proof
const proofs = await this.generateOwnershipProof(["preimage1", "preimage2"], [characterRoot, memoryRoot]);

// create agent NFT
const tokenId = await this.mintToken(proofs, ["eliza_character", "eliza_memory"]);
// const tokenData = await this.getTokenData(tokenId);
elizaLogger.info(`Agent NFT created successfully, token ID: ${tokenId}`);
return tokenId;
} catch (error) {
elizaLogger.error("Error creating agent NFT:", error);
throw error;
}
}
}
19 changes: 19 additions & 0 deletions agent/src/generateNFT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { elizaLogger } from "@elizaos/core";
import { AgentNFTClient } from "./agentNFTClient";
import { parseArguments } from "./agent";

const generateNFT = async () => {
elizaLogger.info("Generating NFT");
try {
const args = parseArguments();
const agentNFTClient = new AgentNFTClient(args.dir);
await agentNFTClient.generateAgentNFT();
} catch (error) {
throw error;
}
};

generateNFT().catch((error) => {
elizaLogger.error("Unhandled error in generateNFT:", error);
process.exit(1);
});
2 changes: 1 addition & 1 deletion agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const startAgents = async () => {
} else if (args.token) {
// load from nft
elizaLogger.info("Starting in NFT mode...");
characters = await loadFromNFT(args.token);
characters = await loadFromNFT(args.token, args.dir);
} else {
// load from default character
elizaLogger.info("Starting with default character...");
Expand Down
2 changes: 1 addition & 1 deletion agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface ParsedArguments {
character?: string;
characters?: string;
token?: string;
validate?: boolean;
dir?: string;
}

export interface TokenData {
Expand Down
Empty file removed agent/tool/createNFT.ts
Empty file.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test": "bash ./scripts/test.sh",
"smokeTests": "bash ./scripts/smokeTests.sh",
"integrationTests": "bash ./scripts/integrationTests.sh",
"start:agent-nft": "pnpm --filter \"@elizaos/agent-nft\" start"
"generate-nft": "pnpm --filter \"@elizaos/agent\" generate-nft --isRoot"
},
"devDependencies": {
"@commitlint/cli": "18.6.1",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1bca358

Please sign in to comment.