From 1bca3583b078968826b7b58e530e50e37a86f767 Mon Sep 17 00:00:00 2001 From: wei Date: Tue, 14 Jan 2025 10:40:52 +0800 Subject: [PATCH] add nft generator --- agent/package.json | 3 +- agent/src/agent.ts | 13 ++- agent/src/agentNFTClient.ts | 171 ++++++++++++++++++++++++++++++------ agent/src/generateNFT.ts | 19 ++++ agent/src/index.ts | 2 +- agent/src/types.ts | 2 +- agent/tool/createNFT.ts | 0 package.json | 2 +- pnpm-lock.yaml | 20 +++++ 9 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 agent/src/generateNFT.ts delete mode 100644 agent/tool/createNFT.ts diff --git a/agent/package.json b/agent/package.json index d1a9899951f..383ff004ebb 100644 --- a/agent/package.json +++ b/agent/package.json @@ -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" @@ -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", diff --git a/agent/src/agent.ts b/agent/src/agent.ts index c2cc748c9b5..d41829f2340 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -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); @@ -609,15 +614,15 @@ export function checkPortAvailable(port: number): Promise { }); } -export async function loadFromNFT(tokenId: string): Promise { - const agentNFTClient = new AgentNFTClient("./data"); +export async function loadFromNFT(tokenId: string, baseDir: string = ""): Promise { + 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}`); diff --git a/agent/src/agentNFTClient.ts b/agent/src/agentNFTClient.ts index 3bc19a68b62..43db3620745 100644 --- a/agent/src/agentNFTClient.ts +++ b/agent/src/agentNFTClient.ts @@ -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, @@ -42,21 +43,36 @@ export class AgentNFTClient { } async getNFTName(): Promise { - 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 { - 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 { @@ -81,6 +97,32 @@ export class AgentNFTClient { } } + async mintToken(proofs: string[], dataDescriptions: string[]): Promise { + 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 { try { const tokenOwner = tokenData.owner.toLowerCase(); @@ -99,20 +141,24 @@ export class AgentNFTClient { } async downloadAndSaveData(tokenId: string, dataHashes: string[], dataDescriptions: string[]): Promise { - 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]])) { @@ -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}`); } } -} \ No newline at end of file + + 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 { + + // TODO: generate proof using preimage and claimedHash, now just return the claimedHash as public input + return claimedHashes; + } + + async generateAgentNFT(): Promise { + 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; + } + } +} diff --git a/agent/src/generateNFT.ts b/agent/src/generateNFT.ts new file mode 100644 index 00000000000..f9ce31612d9 --- /dev/null +++ b/agent/src/generateNFT.ts @@ -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); +}); \ No newline at end of file diff --git a/agent/src/index.ts b/agent/src/index.ts index 37b3923f615..84d19edba86 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -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..."); diff --git a/agent/src/types.ts b/agent/src/types.ts index ddb917b7987..426a7032f32 100644 --- a/agent/src/types.ts +++ b/agent/src/types.ts @@ -2,7 +2,7 @@ export interface ParsedArguments { character?: string; characters?: string; token?: string; - validate?: boolean; + dir?: string; } export interface TokenData { diff --git a/agent/tool/createNFT.ts b/agent/tool/createNFT.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/package.json b/package.json index 241654f1ec7..d463100333c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23cb59e2260..99ca82cffdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: agent: dependencies: + '@0glabs/0g-ts-sdk': + specifier: 0.2.3 + version: 0.2.3(bufferutil@4.0.9)(ethers@6.13.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) '@elizaos/adapter-postgres': specifier: workspace:* version: link:../packages/adapter-postgres @@ -1952,6 +1955,11 @@ packages: peerDependencies: ethers: 6.13.1 + '@0glabs/0g-ts-sdk@0.2.3': + resolution: {integrity: sha512-BT0LD/+QJhC40K9bs+Wd65n71MTpEYIZXQ4I2yr9n9vRMOxDGgBWi1fbiy/Wtcj1V6hvPmbq6qBkslfgwCNsrw==} + peerDependencies: + ethers: 6.13.1 + '@0no-co/graphql.web@1.0.13': resolution: {integrity: sha512-jqYxOevheVTU1S36ZdzAkJIdvRp2m3OYIG5SEoKDw5NI8eVwkoI0D/Q3DYNGmXCxkA6CQuoa7zvMiDPTLqUNuw==} peerDependencies: @@ -19544,6 +19552,18 @@ snapshots: - supports-color - utf-8-validate + '@0glabs/0g-ts-sdk@0.2.3(bufferutil@4.0.9)(ethers@6.13.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + ethers: 6.13.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + open-jsonrpc-provider: 0.2.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + '@0no-co/graphql.web@1.0.13(graphql@16.10.0)': optionalDependencies: graphql: 16.10.0