From 3ff0677f3534f11bdc9e29843f1c936662e3d299 Mon Sep 17 00:00:00 2001 From: JSon Date: Thu, 12 Dec 2024 18:14:10 +0800 Subject: [PATCH 1/5] feat: Add plugin-nft-generation: support for creating Solana NFT collections. --- .env.example | 4 + agent/package.json | 1 + agent/src/index.ts | 13 + packages/core/src/environment.ts | 5 + packages/core/src/types.ts | 6 +- packages/plugin-nft-generation/.npmignore | 6 + .../plugin-nft-generation/eslint.config.mjs | 3 + packages/plugin-nft-generation/package.json | 30 ++ packages/plugin-nft-generation/src/api.ts | 123 +++++++++ .../src/handlers/createCollection.ts | 118 ++++++++ .../src/handlers/createNFT.ts | 178 ++++++++++++ .../src/handlers/verifyNFT.ts | 27 ++ packages/plugin-nft-generation/src/index.ts | 203 ++++++++++++++ .../src/provider/wallet/walletSolana.ts | 228 ++++++++++++++++ packages/plugin-nft-generation/tsconfig.json | 13 + packages/plugin-nft-generation/tsup.config.ts | 21 ++ packages/plugin-node/src/services/awsS3.ts | 3 +- pnpm-lock.yaml | 257 +++++++++++++++++- turbo.json | 3 + 19 files changed, 1238 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-nft-generation/.npmignore create mode 100644 packages/plugin-nft-generation/eslint.config.mjs create mode 100644 packages/plugin-nft-generation/package.json create mode 100644 packages/plugin-nft-generation/src/api.ts create mode 100644 packages/plugin-nft-generation/src/handlers/createCollection.ts create mode 100644 packages/plugin-nft-generation/src/handlers/createNFT.ts create mode 100644 packages/plugin-nft-generation/src/handlers/verifyNFT.ts create mode 100644 packages/plugin-nft-generation/src/index.ts create mode 100644 packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts create mode 100644 packages/plugin-nft-generation/tsconfig.json create mode 100644 packages/plugin-nft-generation/tsup.config.ts diff --git a/.env.example b/.env.example index 2f4957a2b51..e337ecfaedf 100644 --- a/.env.example +++ b/.env.example @@ -147,6 +147,10 @@ EVM_PROVIDER_URL= # Solana SOLANA_PRIVATE_KEY= SOLANA_PUBLIC_KEY= +SOLANA_CLUSTER= # Default: devnet. Solana Cluster: 'devnet' | 'testnet' | 'mainnet-beta' +SOLANA_ADMIN_PRIVATE_KEY= # This wallet is used to verify NFTs +SOLANA_ADMIN_PUBLIC_KEY= # This wallet is used to verify NFTs +SOLANA_VERIFY_TOKEN= # Authentication token for calling the verification API # Fallback Wallet Configuration (deprecated) WALLET_PRIVATE_KEY= diff --git a/agent/package.json b/agent/package.json index e27d4aa5ee5..aab1ec0bc21 100644 --- a/agent/package.json +++ b/agent/package.json @@ -37,6 +37,7 @@ "@ai16z/plugin-goat": "workspace:*", "@ai16z/plugin-icp": "workspace:*", "@ai16z/plugin-image-generation": "workspace:*", + "@ai16z/plugin-nft-generation": "workspace:*", "@ai16z/plugin-node": "workspace:*", "@ai16z/plugin-solana": "workspace:*", "@ai16z/plugin-starknet": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 8aab1b5d38b..5b1d1ac6c41 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -44,6 +44,7 @@ import { solanaPlugin } from "@ai16z/plugin-solana"; import { teePlugin, TEEMode } from "@ai16z/plugin-tee"; import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos"; import { flowPlugin } from "@ai16z/plugin-flow"; +import { nftGenerationPlugin, createNFTApiRouter } from "@ai16z/plugin-nft-generation"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -418,6 +419,12 @@ export async function createAgent( getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) ? evmPlugin : null, + (getSecret(character, "SOLANA_PUBLIC_KEY") || + (getSecret(character, "WALLET_PUBLIC_KEY") && + !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))) && + getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") + ? nftGenerationPlugin + : null, getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, getSecret(character, "COINBASE_COMMERCE_KEY") ? coinbaseCommercePlugin @@ -498,6 +505,12 @@ async function startAgent(character: Character, directClient) { directClient.registerAgent(runtime); + // Support using API to create NFT + // const agents = new Map(); + // agents.set(runtime.agentId, runtime) + // const apiNFTGenerationRouter = createNFTApiRouter(agents); + // directClient?.app?.use(apiNFTGenerationRouter) + return clients; } catch (error) { elizaLogger.error( diff --git a/packages/core/src/environment.ts b/packages/core/src/environment.ts index 8e5f3389366..0758d0d31d9 100644 --- a/packages/core/src/environment.ts +++ b/packages/core/src/environment.ts @@ -124,6 +124,11 @@ export const CharacterSchema = z.object({ nicknames: z.array(z.string()).optional(), }) .optional(), + nft: z + .object({ + prompt: z.string().optional(), + }) + .optional(), }); // Type inference diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d29d0b98056..ee70efe8ef9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -735,6 +735,10 @@ export type Character = { bio: string; nicknames?: string[]; }; + /** Optional NFT prompt */ + nft?: { + prompt: string; + } }; /** @@ -1124,7 +1128,7 @@ export interface IPdfService extends Service { } export interface IAwsS3Service extends Service { - uploadFile(imagePath: string, useSignedUrl: boolean, expiresIn: number ): Promise<{ + uploadFile(imagePath: string, subDirectory: string, useSignedUrl: boolean, expiresIn: number ): Promise<{ success: boolean; url?: string; error?: string; diff --git a/packages/plugin-nft-generation/.npmignore b/packages/plugin-nft-generation/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-nft-generation/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-nft-generation/eslint.config.mjs b/packages/plugin-nft-generation/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-nft-generation/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-nft-generation/package.json b/packages/plugin-nft-generation/package.json new file mode 100644 index 00000000000..bea518c9aa6 --- /dev/null +++ b/packages/plugin-nft-generation/package.json @@ -0,0 +1,30 @@ +{ + "name": "@ai16z/plugin-nft-generation", + "version": "0.1.5-alpha.5", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@ai16z/plugin-image-generation": "workspace:*", + "@ai16z/plugin-node": "workspace:*", + "@metaplex-foundation/mpl-token-metadata": "^3.3.0", + "@metaplex-foundation/mpl-toolbox": "^0.9.4", + "@metaplex-foundation/umi": "^0.9.2", + "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", + "@solana-developers/helpers": "^2.5.6", + "@solana/web3.js": "1.95.5", + "bs58": "6.0.0", + "express": "4.21.1", + "node-cache": "5.1.2", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint . --fix" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-nft-generation/src/api.ts b/packages/plugin-nft-generation/src/api.ts new file mode 100644 index 00000000000..b3e44b03235 --- /dev/null +++ b/packages/plugin-nft-generation/src/api.ts @@ -0,0 +1,123 @@ +import express from "express"; + +import { AgentRuntime } from "@ai16z/eliza"; +import { createCollection } from "./handlers/createCollection.ts"; +import { createNFT } from "./handlers/createNFT.ts"; +import { verifyNFT } from "./handlers/verifyNFT.ts"; + +export function createNFTApiRouter(agents: Map) { + const router = express.Router(); + + router.post( + "/api/nft-generation/create-collection", + async (req: express.Request, res: express.Response) => { + const agentId = req.body.agentId; + const fee = req.body.fee || 0; + const runtime = agents.get(agentId); + if (!runtime) { + res.status(404).send("Agent not found"); + return; + } + try { + const collectionAddressRes = await createCollection({ + runtime, + collectionName: runtime.character.name, + fee + }); + + res.json({ + success: true, + data: collectionAddressRes, + }); + } catch (e: any) { + console.log(e); + res.json({ + success: false, + data: JSON.stringify(e), + }); + } + } + ); + + router.post( + "/api/nft-generation/create-nft", + async (req: express.Request, res: express.Response) => { + const agentId = req.body.agentId; + const collectionName = req.body.collectionName; + const collectionAddress = req.body.collectionAddress; + const collectionAdminPublicKey = req.body.collectionAdminPublicKey; + const collectionFee = req.body.collectionFee; + const tokenId = req.body.tokenId; + const runtime = agents.get(agentId); + if (!runtime) { + res.status(404).send("Agent not found"); + return; + } + + try { + const nftRes = await createNFT({ + runtime, + collectionName, + collectionAddress, + collectionAdminPublicKey, + collectionFee, + tokenId, + }); + + res.json({ + success: true, + data: nftRes, + }); + } catch (e: any) { + console.log(e); + res.json({ + success: false, + data: JSON.stringify(e), + }); + } + } + ); + + + router.post( + "/api/nft-generation/verify-nft", + async (req: express.Request, res: express.Response) => { + const agentId = req.body.agentId; + const collectionAddress = req.body.collectionAddress; + const NFTAddress = req.body.nftAddress; + const token = req.body.token; + + const runtime = agents.get(agentId); + if (!runtime) { + res.status(404).send("Agent not found"); + return; + } + const verifyToken = runtime.getSetting('SOLANA_VERIFY_TOKEN') + if (token !== verifyToken) { + res.status(401).send(" Access denied for translation"); + return; + } + try { + const {success} = await verifyNFT({ + runtime, + collectionAddress, + NFTAddress, + }); + + res.json({ + success: true, + data: success ? 'verified' : 'unverified', + }); + } catch (e: any) { + console.log(e); + res.json({ + success: false, + data: JSON.stringify(e), + }); + } + } + ); + + + return router; +} diff --git a/packages/plugin-nft-generation/src/handlers/createCollection.ts b/packages/plugin-nft-generation/src/handlers/createCollection.ts new file mode 100644 index 00000000000..21960c2a2d0 --- /dev/null +++ b/packages/plugin-nft-generation/src/handlers/createCollection.ts @@ -0,0 +1,118 @@ +import { AwsS3Service } from "@ai16z/plugin-node"; +import { + composeContext, + elizaLogger, + generateImage, + getEmbeddingZeroVector, + IAgentRuntime, + Memory, + ServiceType, + stringToUuid, +} from "@ai16z/eliza"; +import { + saveBase64Image, + saveHeuristImage, +} from "@ai16z/plugin-image-generation"; +import { PublicKey } from "@solana/web3.js"; +import WalletSolana from "../provider/wallet/walletSolana.ts"; + +const collectionImageTemplate = ` +Generate a logo with the text "{{collectionName}}", using orange as the main color, with a sci-fi and mysterious background theme +`; + +export async function createCollection({ + runtime, + collectionName, + fee, +}: { + runtime: IAgentRuntime; + collectionName: string; + fee?: number; +}) { + const userId = runtime.agentId; + elizaLogger.log("User ID:", userId); + const awsS3Service: AwsS3Service = runtime.getService(ServiceType.AWS_S3); + const agentName = runtime.character.name; + const roomId = stringToUuid("nft_generate_room-" + agentName); + // Create memory for the message + const memory: Memory = { + agentId: userId, + userId, + roomId, + content: { + text: "", + + source: "nft-generator", + }, + createdAt: Date.now(), + embedding: getEmbeddingZeroVector(), + }; + const state = await runtime.composeState(memory, { + collectionName, + }); + + const prompt = composeContext({ + state, + template: collectionImageTemplate, + }); + const images = await generateImage( + { + prompt, + width: 300, + height: 300, + }, + runtime + ); + if (images.success && images.data && images.data.length > 0) { + const image = images.data[0]; + const filename = `collection-image`; + if (image.startsWith("http")) { + elizaLogger.log("Generating image url:", image); + } + // Choose save function based on image data format + const filepath = image.startsWith("http") + ? await saveHeuristImage(image, filename) + : saveBase64Image(image, filename); + + const logoPath = await awsS3Service.uploadFile( + filepath, + `/${collectionName}`, + false + ); + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + const adminPublicKey = runtime.getSetting("SOLANA_ADMIN_PUBLIC_KEY"); + const collectionInfo = { + name: `${collectionName}`, + symbol: `${collectionName.toUpperCase()[0]}`, + adminPublicKey, + fee: fee || 0, + uri: "", + }; + const jsonFilePath = await awsS3Service.uploadJson( + { + name: collectionInfo.name, + description: `${collectionInfo.name}`, + image: logoPath.url, + }, + "metadata.json", + `${collectionName}` + ); + collectionInfo.uri = jsonFilePath.url; + + const wallet = new WalletSolana(new PublicKey(publicKey), privateKey); + + const collectionAddressRes = await wallet.createCollection({ + ...collectionInfo, + }); + + return { + network: "solana", + address: collectionAddressRes.address, + link: collectionAddressRes.link, + collectionInfo, + }; + } + + return; +} diff --git a/packages/plugin-nft-generation/src/handlers/createNFT.ts b/packages/plugin-nft-generation/src/handlers/createNFT.ts new file mode 100644 index 00000000000..0c6767f2669 --- /dev/null +++ b/packages/plugin-nft-generation/src/handlers/createNFT.ts @@ -0,0 +1,178 @@ +import { AwsS3Service } from "@ai16z/plugin-node"; +import { + composeContext, + elizaLogger, + generateImage, + generateText, + getEmbeddingZeroVector, + IAgentRuntime, + Memory, + ModelClass, + ServiceType, + stringToUuid, +} from "@ai16z/eliza"; +import { + saveBase64Image, + saveHeuristImage, +} from "@ai16z/plugin-image-generation"; +import { PublicKey } from "@solana/web3.js"; +import WalletSolana from "../provider/wallet/walletSolana.ts"; + +const nftTemplate = ` +# Areas of Expertise +{{knowledge}} + +# About {{agentName}} (@{{twitterUserName}}): +{{bio}} +{{lore}} +{{topics}} + +{{providers}} + +{{characterPostExamples}} + +{{postDirections}} +# Task: Generate an image to Prompt the {{agentName}}'s appearance, with the total character count MUST be less than 280. +`; + +export async function createNFTMetadata({ + runtime, + collectionName, + collectionAdminPublicKey, + collectionFee, + tokenId, +}: { + runtime: IAgentRuntime; + collectionName: string; + collectionAdminPublicKey: string; + collectionFee: number; + tokenId: number; +}) { + const userId = runtime.agentId; + elizaLogger.log("User ID:", userId); + const awsS3Service: AwsS3Service = runtime.getService(ServiceType.AWS_S3); + const agentName = runtime.character.name; + const roomId = stringToUuid("nft_generate_room-" + agentName); + // Create memory for the message + const memory: Memory = { + agentId: userId, + userId, + roomId, + content: { + text: "", + source: "nft-generator", + }, + createdAt: Date.now(), + embedding: getEmbeddingZeroVector(), + }; + const state = await runtime.composeState(memory, { + collectionName, + }); + + const context = composeContext({ + state, + template: nftTemplate, + }); + + let nftPrompt = await generateText({ + runtime, + context, + modelClass: ModelClass.MEDIUM, + }); + + nftPrompt += runtime.character?.nft?.prompt || ""; + nftPrompt += "The image should only feature one person."; + + const images = await generateImage( + { + prompt: nftPrompt, + width: 1024, + height: 1024, + }, + runtime + ); + elizaLogger.log("NFT Prompt:", nftPrompt); + if (images.success && images.data && images.data.length > 0) { + const image = images.data[0]; + const filename = `${tokenId}`; + if (image.startsWith("http")) { + elizaLogger.log("Generating image url:", image); + } + // Choose save function based on image data format + const filepath = image.startsWith("http") + ? await saveHeuristImage(image, filename) + : saveBase64Image(image, filename); + const nftImage = await awsS3Service.uploadFile( + filepath, + `/${collectionName}/items/${tokenId}`, + false + ); + const nftInfo = { + name: `${collectionName} #${tokenId}`, + description: `${collectionName} #${tokenId}`, + symbol: `${collectionName.toUpperCase()[0]}`, + adminPublicKey: collectionAdminPublicKey, + fee: collectionFee, + uri: "", + }; + const jsonFilePath = await awsS3Service.uploadJson( + { + name: nftInfo.name, + description: nftInfo.description, + image: nftImage.url, + }, + "metadata.json", + `/${collectionName}/items/${tokenId}` + ); + + nftInfo.uri = jsonFilePath.url; + return nftInfo; + } + return null; +} + +export async function createNFT({ + runtime, + collectionName, + collectionAddress, + collectionAdminPublicKey, + collectionFee, + tokenId, +}: { + runtime: IAgentRuntime; + collectionName: string; + collectionAddress: string; + collectionAdminPublicKey: string; + collectionFee: number; + tokenId: number; +}) { + const nftInfo = await createNFTMetadata({ + runtime, + collectionName, + collectionAdminPublicKey, + collectionFee, + tokenId, + }); + if (nftInfo) { + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + + const wallet = new WalletSolana(new PublicKey(publicKey), privateKey); + + const nftAddressRes = await wallet.mintNFT({ + name: nftInfo.name, + uri: nftInfo.uri, + collectionAddress, + adminPublicKey: collectionAdminPublicKey, + fee: collectionFee, + }); + elizaLogger.log("NFT ID:", nftAddressRes.address); + return { + network: "solana", + address: nftAddressRes.address, + link: nftAddressRes.link, + nftInfo, + }; + } + return; +} diff --git a/packages/plugin-nft-generation/src/handlers/verifyNFT.ts b/packages/plugin-nft-generation/src/handlers/verifyNFT.ts new file mode 100644 index 00000000000..792c38ffc5e --- /dev/null +++ b/packages/plugin-nft-generation/src/handlers/verifyNFT.ts @@ -0,0 +1,27 @@ +import { IAgentRuntime } from "@ai16z/eliza"; +import { PublicKey } from "@solana/web3.js"; +import WalletSolana from "../provider/wallet/walletSolana.ts"; + +export async function verifyNFT({ + runtime, + collectionAddress, + NFTAddress, +}: { + runtime: IAgentRuntime; + collectionAddress: string; + NFTAddress: string; +}) { + const adminPublicKey = runtime.getSetting("SOLANA_ADMIN_PUBLIC_KEY"); + const adminPrivateKey = runtime.getSetting("SOLANA_ADMIN_PRIVATE_KEY"); + const adminWallet = new WalletSolana( + new PublicKey(adminPublicKey), + adminPrivateKey + ); + await adminWallet.verifyNft({ + collectionAddress, + nftAddress: NFTAddress, + }); + return { + success: true, + }; +} diff --git a/packages/plugin-nft-generation/src/index.ts b/packages/plugin-nft-generation/src/index.ts new file mode 100644 index 00000000000..f5f442b97ee --- /dev/null +++ b/packages/plugin-nft-generation/src/index.ts @@ -0,0 +1,203 @@ +import { + Action, + elizaLogger, + HandlerCallback, + IAgentRuntime, + Memory, + Plugin, + State, +} from "@ai16z/eliza"; + +import { createCollection } from "./handlers/createCollection.ts"; +import { createNFT } from "./handlers/createNFT.ts"; +import { verifyNFT } from "./handlers/verifyNFT.ts"; + +export * from "./provider/wallet/walletSolana.ts"; +export * from "./api.ts"; + + +export async function sleep(ms: number = 3000) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +const nftCollectionGeneration: Action = { + name: "GENERATE_COLLECTION", + similes: [ + "COLLECTION_GENERATION", + "COLLECTION_GEN", + "CREATE_COLLECTION", + "MAKE_COLLECTION", + "GENERATE_COLLECTION", + ], + description: "Generate an NFT collection for the message", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + const AwsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); + const AwsSecretAccessKeyOk = !!runtime.getSetting( + "AWS_SECRET_ACCESS_KEY" + ); + const AwsRegionOk = !!runtime.getSetting("AWS_REGION"); + const AwsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); + + return ( + AwsAccessKeyIdOk || + AwsSecretAccessKeyOk || + AwsRegionOk || + AwsS3BucketOk + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: { [key: string]: unknown }, + callback: HandlerCallback + ) => { + try { + elizaLogger.log("Composing state for message:", message); + const userId = runtime.agentId; + elizaLogger.log("User ID:", userId); + + const collectionAddressRes = await createCollection({ + runtime, + collectionName: runtime.character.name, + }); + + const collectionInfo = collectionAddressRes.collectionInfo; + + elizaLogger.log("Collection Address:", collectionAddressRes); + + const nftRes = await createNFT({ + runtime, + collectionName: collectionInfo.name, + collectionAddress: collectionAddressRes.address, + collectionAdminPublicKey: collectionInfo.adminPublicKey, + collectionFee: collectionInfo.fee, + tokenId: 1, + }); + + elizaLogger.log("NFT Address:", nftRes); + + + callback({ + text: `Congratulations to you! 🎉🎉🎉 \nCollection : ${collectionAddressRes.link}\n NFT: ${nftRes.link}`, //caption.description, + attachments: [], + }); + await sleep(15000); + await verifyNFT({ + runtime, + collectionAddress: collectionAddressRes.address, + NFTAddress: nftRes.address, + }); + return []; + } catch (e: any) { + console.log(e); + } + + // callback(); + }, + examples: [ + // TODO: We want to generate images in more abstract ways, not just when asked to generate an image + + [ + { + user: "{{user1}}", + content: { text: "Generate a collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "Here's the collection you requested.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Generate a collection using {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "We've successfully created a collection.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Create a collection using {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "Here's the collection you requested.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Build a Collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been successfully built.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Assemble a collection with {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been assembled", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Make a collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been produced successfully.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Compile a collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been compiled.", + action: "GENERATE_COLLECTION", + }, + }, + ], + ], +} as Action; + +export const nftGenerationPlugin: Plugin = { + name: "nftCollectionGeneration", + description: "Generate NFT Collections", + actions: [nftCollectionGeneration], + evaluators: [], + providers: [], +}; diff --git a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts new file mode 100644 index 00000000000..e88e36beb5d --- /dev/null +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -0,0 +1,228 @@ +import NodeCache from "node-cache"; +import { + Cluster, + clusterApiUrl, + Connection, + LAMPORTS_PER_SOL, + PublicKey, +} from "@solana/web3.js"; +import { + createNft, + findMetadataPda, + mplTokenMetadata, + updateV1, + verifyCollectionV1, +} from "@metaplex-foundation/mpl-token-metadata"; +import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; +import { + generateSigner, + keypairIdentity, + percentAmount, + publicKey, + sol, + TransactionBuilder, + Umi, +} from "@metaplex-foundation/umi"; +import { getExplorerLink } from "@solana-developers/helpers"; +import { transferSol } from "@metaplex-foundation/mpl-toolbox"; +import bs58 from "bs58"; +import { elizaLogger } from "@ai16z/eliza"; + +export class WalletSolana { + private cache: NodeCache; + private umi: Umi; + private cluster: Cluster; + + constructor( + private walletPublicKey: PublicKey, + private walletPrivateKeyKey: string, + private connection?: Connection + ) { + this.cache = new NodeCache({ stdTTL: 300 }); // Cache TTL set to 5 minutes + + if (!connection) { + this.cluster = (process.env.SOLANA_CLUSTER as Cluster) || "devnet" + this.connection = new Connection(clusterApiUrl(this.cluster), { + commitment: "finalized", + }); + } + const umi = createUmi(this.connection.rpcEndpoint); + umi.use(mplTokenMetadata()); + const umiUser = umi.eddsa.createKeypairFromSecretKey( + this.privateKeyUint8Array + ); + umi.use(keypairIdentity(umiUser)); + this.umi = umi; + } + + async getBalance() { + let balance = await this.connection.getBalance(this.walletPublicKey); + return { + value: balance, + formater: `${balance / LAMPORTS_PER_SOL} SOL`, + }; + } + + get privateKeyUint8Array() { + return bs58.decode(this.walletPrivateKeyKey); + } + + async createCollection({ + name, + symbol, + adminPublicKey, + uri, + fee, + }: { + name: string; + symbol: string; + adminPublicKey: string; + uri: string; + fee: number; + }): Promise { + const collectionMint = generateSigner(this.umi); + elizaLogger.log("collectionMint", collectionMint, percentAmount(5)); + let transaction = new TransactionBuilder(); + const info = { + name, + symbol, + uri, + }; + console.log(`Metadata uploaded: ${uri}`); + transaction = transaction.add( + createNft(this.umi, { + ...info, + mint: collectionMint, + sellerFeeBasisPoints: percentAmount(fee), + isCollection: true, + }) + ); + + transaction = transaction.add( + updateV1(this.umi, { + mint: collectionMint.publicKey, + newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key + }) + ); + + await transaction.sendAndConfirm(this.umi, { + confirm: {}, + }); + + // await sleep(5000); + // const createdCollectionNft = await fetchDigitalAsset( + // this.umi, + // collectionMint.publicKey + // ); + const address = collectionMint.publicKey; + return { + link: getExplorerLink("address", address, this.cluster), + address, + }; + } + + async mintNFT({ + collectionAddress, + adminPublicKey, + name, + uri, + fee, + }: { + collectionAddress: string; + adminPublicKey: string; + name: string; + uri: string; + fee: number; + }): Promise { + const umi = this.umi; + const mint = generateSigner(umi); + + let transaction = new TransactionBuilder(); + console.log("collectionAddress", collectionAddress); + const collectionAddressKey = publicKey(collectionAddress); + // Add SOL transfer instruction (0.1 SOL) + const receiverAddressKey = publicKey(adminPublicKey); // Replace with actual receiver address + transaction = transaction.add( + transferSol(umi, { + source: umi.identity, + destination: receiverAddressKey, + amount: sol(0.1), + }) + ); + const info = { + name, + uri, + }; + transaction = transaction.add( + createNft(umi, { + mint, + ...info, + sellerFeeBasisPoints: percentAmount(fee), + collection: { + key: collectionAddressKey, + verified: false, + }, + }) + ); + + transaction = transaction.add( + updateV1(umi, { + mint: mint.publicKey, + newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key + }) + ); + + await transaction.sendAndConfirm(umi); + // + // await sleep(); + // const createdNft = await fetchDigitalAsset(umi, mint.publicKey); + // + // console.log( + // `🖼️ Created NFT! Address is ${getExplorerLink( + // "address", + // createdNft.mint.publicKey, + // "devnet" + // )}` + // ); + const address = mint.publicKey; + return { + link: getExplorerLink("address", address, this.cluster), + address, + }; + } + + async verifyNft({ + collectionAddress, + nftAddress, + }: { + collectionAddress: string; + nftAddress: string; + }) { + const umi = this.umi; + const collectionAddressKey = publicKey(collectionAddress); + const nftAddressKey = publicKey(nftAddress); + console.log("collectionAddress", collectionAddress); + console.log("nftAddress", nftAddress); + + let transaction = new TransactionBuilder(); + transaction = transaction.add( + verifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: nftAddressKey }), + collectionMint: collectionAddressKey, + authority: umi.identity, + }) + ); + + await transaction.sendAndConfirm(umi); + + console.log( + `✅ NFT ${nftAddress} verified as member of collection ${collectionAddress}! See Explorer at ${getExplorerLink( + "address", + nftAddress, + this.cluster + )}` + ); + } +} + +export default WalletSolana; diff --git a/packages/plugin-nft-generation/tsconfig.json b/packages/plugin-nft-generation/tsconfig.json new file mode 100644 index 00000000000..834c4dce269 --- /dev/null +++ b/packages/plugin-nft-generation/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-nft-generation/tsup.config.ts b/packages/plugin-nft-generation/tsup.config.ts new file mode 100644 index 00000000000..1a96f24afa1 --- /dev/null +++ b/packages/plugin-nft-generation/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "safe-buffer", + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-node/src/services/awsS3.ts b/packages/plugin-node/src/services/awsS3.ts index 57600ada5bd..c8622d13ab0 100644 --- a/packages/plugin-node/src/services/awsS3.ts +++ b/packages/plugin-node/src/services/awsS3.ts @@ -63,6 +63,7 @@ export class AwsS3Service extends Service implements IAwsS3Service { async uploadFile( filePath: string, + subDirectory: string = '', useSignedUrl: boolean = false, expiresIn: number = 900 ): Promise { @@ -85,7 +86,7 @@ export class AwsS3Service extends Service implements IAwsS3Service { const baseFileName = `${Date.now()}-${path.basename(filePath)}`; // Determine storage path based on public access - const fileName =`${this.fileUploadPath}/${baseFileName}`.replaceAll('//', '/'); + const fileName =`${this.fileUploadPath}${subDirectory}/${baseFileName}`.replaceAll('//', '/'); // Set upload parameters const uploadParams = { Bucket: this.bucket, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6e2535fef..9ced57f4b3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: '@ai16z/plugin-intiface': specifier: workspace:* version: link:../packages/plugin-intiface + '@ai16z/plugin-nft-generation': + specifier: workspace:* + version: link:../packages/plugin-nft-generation '@ai16z/plugin-node': specifier: workspace:* version: link:../packages/plugin-node @@ -1080,6 +1083,51 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-nft-generation: + dependencies: + '@ai16z/eliza': + specifier: workspace:* + version: link:../core + '@ai16z/plugin-image-generation': + specifier: workspace:* + version: link:../plugin-image-generation + '@ai16z/plugin-node': + specifier: workspace:* + version: link:../plugin-node + '@metaplex-foundation/mpl-token-metadata': + specifier: ^3.3.0 + version: 3.3.0(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/mpl-toolbox': + specifier: ^0.9.4 + version: 0.9.4(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi': + specifier: ^0.9.2 + version: 0.9.2 + '@metaplex-foundation/umi-bundle-defaults': + specifier: ^0.9.2 + version: 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(encoding@0.1.13) + '@solana-developers/helpers': + specifier: ^2.5.6 + version: 2.5.6(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10) + '@solana/web3.js': + specifier: 1.95.5 + version: 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + bs58: + specifier: 6.0.0 + version: 6.0.0 + express: + specifier: 4.21.1 + version: 4.21.1 + node-cache: + specifier: 5.1.2 + version: 5.1.2 + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.0)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-node: dependencies: '@ai16z/eliza': @@ -4260,6 +4308,92 @@ packages: '@mermaid-js/parser@0.3.0': resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==} + '@metaplex-foundation/mpl-token-metadata@3.3.0': + resolution: {integrity: sha512-t5vO8Wr3ZZZPGrVrGNcosX5FMkwQSgBiVMQMRNDG2De7voYFJmIibD5jdG05EoQ4Y5kZVEiwhYaO+wJB3aO5AA==} + peerDependencies: + '@metaplex-foundation/umi': '>= 0.8.2 < 1' + + '@metaplex-foundation/mpl-toolbox@0.9.4': + resolution: {integrity: sha512-fd6JxfoLbj/MM8FG2x91KYVy1U6AjBQw4qjt7+Da3trzQaWnSaYHDcYRG/53xqfvZ9qofY1T2t53GXPlD87lnQ==} + peerDependencies: + '@metaplex-foundation/umi': '>= 0.8.2 < 1' + + '@metaplex-foundation/umi-bundle-defaults@0.9.2': + resolution: {integrity: sha512-kV3tfvgvRjVP1p9OFOtH+ibOtN9omVJSwKr0We4/9r45e5LTj+32su0V/rixZUkG1EZzzOYBsxhtIE0kIw/Hrw==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + '@solana/web3.js': ^1.72.0 + + '@metaplex-foundation/umi-downloader-http@0.9.2': + resolution: {integrity: sha512-tzPT9hBwenzTzAQg07rmsrqZfgguAXELbcJrsYMoASp5VqWFXYIP00g94KET6XLjWUXH4P1J2zoa6hGennPXHA==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + + '@metaplex-foundation/umi-eddsa-web3js@0.9.2': + resolution: {integrity: sha512-hhPCxXbYIp4BC4z9gK78sXpWLkNSrfv4ndhF5ruAkdIp7GcRVYKj0QnOUO6lGYGiIkNlw20yoTwOe1CT//OfTQ==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + '@solana/web3.js': ^1.72.0 + + '@metaplex-foundation/umi-http-fetch@0.9.2': + resolution: {integrity: sha512-YCZuBu24T9ZzEDe4+w12LEZm/fO9pkyViZufGgASC5NX93814Lvf6Ssjn/hZzjfA7CvZbvLFbmujc6CV3Q/m9Q==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + + '@metaplex-foundation/umi-options@0.8.9': + resolution: {integrity: sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==} + + '@metaplex-foundation/umi-program-repository@0.9.2': + resolution: {integrity: sha512-g3+FPqXEmYsBa8eETtUE2gb2Oe3mqac0z3/Ur1TvAg5TtIy3mzRzOy/nza+sgzejnfcxcVg835rmpBaxpBnjDA==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + + '@metaplex-foundation/umi-public-keys@0.8.9': + resolution: {integrity: sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==} + + '@metaplex-foundation/umi-rpc-chunk-get-accounts@0.9.2': + resolution: {integrity: sha512-YRwVf6xH0jPBAUgMhEPi+UbjioAeqTXmjsN2TnmQCPAmHbrHrMRj0rlWYwFLWAgkmoxazYrXP9lqOFRrfOGAEA==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + + '@metaplex-foundation/umi-rpc-web3js@0.9.2': + resolution: {integrity: sha512-MqcsBz8B4wGl6jxsf2Jo/rAEpYReU9VCSR15QSjhvADHMmdFxCIZCCAgE+gDE2Vuanfl437VhOcP3g5Uw8C16Q==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + '@solana/web3.js': ^1.72.0 + + '@metaplex-foundation/umi-serializer-data-view@0.9.2': + resolution: {integrity: sha512-5vGptadJxUxvUcyrwFZxXlEc6Q7AYySBesizCtrBFUY8w8PnF2vzmS45CP1MLySEATNH6T9mD4Rs0tLb87iQyA==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + + '@metaplex-foundation/umi-serializers-core@0.8.9': + resolution: {integrity: sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==} + + '@metaplex-foundation/umi-serializers-encodings@0.8.9': + resolution: {integrity: sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==} + + '@metaplex-foundation/umi-serializers-numbers@0.8.9': + resolution: {integrity: sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==} + + '@metaplex-foundation/umi-serializers@0.9.0': + resolution: {integrity: sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==} + + '@metaplex-foundation/umi-transaction-factory-web3js@0.9.2': + resolution: {integrity: sha512-fR1Kf21uylMFd1Smkltmj4jTNxhqSWf416owsJ+T+cvJi2VCOcOwq/3UFzOrpz78fA0RhsajKYKj0HYsRnQI1g==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + '@solana/web3.js': ^1.72.0 + + '@metaplex-foundation/umi-web3js-adapters@0.9.2': + resolution: {integrity: sha512-RQqUTtHYY9fmEMnq7s3Hiv/81flGaoI0ZVVoafnFVaQLnxU6QBKxtboRZHk43XtD9CiFh5f9izrMJX7iK7KlOA==} + peerDependencies: + '@metaplex-foundation/umi': ^0.9.2 + '@solana/web3.js': ^1.72.0 + + '@metaplex-foundation/umi@0.9.2': + resolution: {integrity: sha512-9i4Acm4pruQfJcpRrc2EauPBwkfDN0I9QTvJyZocIlKgoZwD6A6wH0PViH1AjOVG5CQCd1YI3tJd5XjYE1ElBw==} + '@motionone/animation@10.18.0': resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==} @@ -5890,6 +6024,9 @@ packages: resolution: {integrity: sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==} engines: {node: '>=16.0.0'} + '@solana-developers/helpers@2.5.6': + resolution: {integrity: sha512-NPWZblVMl4LuVVSJOZG0ZF0VYnrMUjCyMNTiGwNUXPK2WWYJCqpuDyzs/PMqwvM4gMTjk4pEToBX8N2UxDvZkQ==} + '@solana/buffer-layout-utils@0.2.0': resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} engines: {node: '>= 10'} @@ -21254,6 +21391,108 @@ snapshots: dependencies: langium: 3.0.0 + '@metaplex-foundation/mpl-token-metadata@3.3.0(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/mpl-toolbox': 0.9.4(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/mpl-toolbox@0.9.4(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/umi-bundle-defaults@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(encoding@0.1.13)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + '@metaplex-foundation/umi-downloader-http': 0.9.2(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi-eddsa-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@metaplex-foundation/umi-http-fetch': 0.9.2(@metaplex-foundation/umi@0.9.2)(encoding@0.1.13) + '@metaplex-foundation/umi-program-repository': 0.9.2(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi-rpc-chunk-get-accounts': 0.9.2(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi-rpc-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@metaplex-foundation/umi-serializer-data-view': 0.9.2(@metaplex-foundation/umi@0.9.2) + '@metaplex-foundation/umi-transaction-factory-web3js': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - encoding + + '@metaplex-foundation/umi-downloader-http@0.9.2(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/umi-eddsa-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@noble/curves': 1.7.0 + '@solana/web3.js': 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + + '@metaplex-foundation/umi-http-fetch@0.9.2(@metaplex-foundation/umi@0.9.2)(encoding@0.1.13)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@metaplex-foundation/umi-options@0.8.9': {} + + '@metaplex-foundation/umi-program-repository@0.9.2(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/umi-public-keys@0.8.9': + dependencies: + '@metaplex-foundation/umi-serializers-encodings': 0.8.9 + + '@metaplex-foundation/umi-rpc-chunk-get-accounts@0.9.2(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/umi-rpc-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + + '@metaplex-foundation/umi-serializer-data-view@0.9.2(@metaplex-foundation/umi@0.9.2)': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + + '@metaplex-foundation/umi-serializers-core@0.8.9': {} + + '@metaplex-foundation/umi-serializers-encodings@0.8.9': + dependencies: + '@metaplex-foundation/umi-serializers-core': 0.8.9 + + '@metaplex-foundation/umi-serializers-numbers@0.8.9': + dependencies: + '@metaplex-foundation/umi-serializers-core': 0.8.9 + + '@metaplex-foundation/umi-serializers@0.9.0': + dependencies: + '@metaplex-foundation/umi-options': 0.8.9 + '@metaplex-foundation/umi-public-keys': 0.8.9 + '@metaplex-foundation/umi-serializers-core': 0.8.9 + '@metaplex-foundation/umi-serializers-encodings': 0.8.9 + '@metaplex-foundation/umi-serializers-numbers': 0.8.9 + + '@metaplex-foundation/umi-transaction-factory-web3js@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + '@metaplex-foundation/umi-web3js-adapters': 0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + + '@metaplex-foundation/umi-web3js-adapters@0.9.2(@metaplex-foundation/umi@0.9.2)(@solana/web3.js@1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))': + dependencies: + '@metaplex-foundation/umi': 0.9.2 + '@solana/web3.js': 1.95.5(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + buffer: 6.0.3 + + '@metaplex-foundation/umi@0.9.2': + dependencies: + '@metaplex-foundation/umi-options': 0.8.9 + '@metaplex-foundation/umi-public-keys': 0.8.9 + '@metaplex-foundation/umi-serializers': 0.9.0 + '@motionone/animation@10.18.0': dependencies: '@motionone/easing': 10.18.0 @@ -21856,7 +22095,7 @@ snapshots: '@octokit/request-error': 3.0.3 '@octokit/types': 9.3.2 is-plain-object: 5.0.0 - node-fetch: 2.6.7(encoding@0.1.13) + node-fetch: 2.7.0(encoding@0.1.13) universal-user-agent: 6.0.1 transitivePeerDependencies: - encoding @@ -23310,6 +23549,20 @@ snapshots: '@smithy/types': 3.7.2 tslib: 2.8.1 + '@solana-developers/helpers@2.5.6(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/spl-token': 0.4.9(@solana/web3.js@1.95.8(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.95.8(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3) + '@solana/web3.js': 1.95.8(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + bs58: 6.0.0 + dotenv: 16.4.5 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -28214,7 +28467,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.0(supports-color@5.5.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: diff --git a/turbo.json b/turbo.json index e6a1ba00faf..2992e58a18d 100644 --- a/turbo.json +++ b/turbo.json @@ -15,6 +15,9 @@ "outputs": ["dist/**"], "dependsOn": ["@ai16z/plugin-trustdb#build"] }, + "@ai16z/plugin-nft-generation#build": { + "dependsOn": ["@ai16z/plugin-node#build"] + }, "eliza-docs#build": { "outputs": ["build/**"] }, From 87628ece12c3b8a371f88eeed0a998ffae493efe Mon Sep 17 00:00:00 2001 From: JSon Date: Thu, 12 Dec 2024 20:23:45 +0800 Subject: [PATCH 2/5] feat: nft symbol --- packages/plugin-nft-generation/src/handlers/createNFT.ts | 3 ++- .../src/provider/wallet/walletSolana.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-nft-generation/src/handlers/createNFT.ts b/packages/plugin-nft-generation/src/handlers/createNFT.ts index 0c6767f2669..5ab5321b23b 100644 --- a/packages/plugin-nft-generation/src/handlers/createNFT.ts +++ b/packages/plugin-nft-generation/src/handlers/createNFT.ts @@ -110,7 +110,7 @@ export async function createNFTMetadata({ const nftInfo = { name: `${collectionName} #${tokenId}`, description: `${collectionName} #${tokenId}`, - symbol: `${collectionName.toUpperCase()[0]}`, + symbol: `#${tokenId}`, adminPublicKey: collectionAdminPublicKey, fee: collectionFee, uri: "", @@ -162,6 +162,7 @@ export async function createNFT({ const nftAddressRes = await wallet.mintNFT({ name: nftInfo.name, uri: nftInfo.uri, + symbol: nftInfo.symbol, collectionAddress, adminPublicKey: collectionAdminPublicKey, fee: collectionFee, diff --git a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts index e88e36beb5d..6205b2aecea 100644 --- a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -41,7 +41,7 @@ export class WalletSolana { this.cache = new NodeCache({ stdTTL: 300 }); // Cache TTL set to 5 minutes if (!connection) { - this.cluster = (process.env.SOLANA_CLUSTER as Cluster) || "devnet" + this.cluster = (process.env.SOLANA_CLUSTER as Cluster) || "devnet"; this.connection = new Connection(clusterApiUrl(this.cluster), { commitment: "finalized", }); @@ -125,12 +125,14 @@ export class WalletSolana { collectionAddress, adminPublicKey, name, + symbol, uri, fee, }: { collectionAddress: string; adminPublicKey: string; name: string; + symbol: string; uri: string; fee: number; }): Promise { @@ -152,6 +154,7 @@ export class WalletSolana { const info = { name, uri, + symbol, }; transaction = transaction.add( createNft(umi, { From 1a6629a428f7de17ab9887299538f11e694a9e8e Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 13 Dec 2024 01:46:45 +0800 Subject: [PATCH 3/5] feat: feat: add API for NFT metadata creation. --- packages/plugin-nft-generation/src/api.ts | 44 ++++++++++++++++++- .../src/handlers/createNFT.ts | 5 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/plugin-nft-generation/src/api.ts b/packages/plugin-nft-generation/src/api.ts index b3e44b03235..83091cf8976 100644 --- a/packages/plugin-nft-generation/src/api.ts +++ b/packages/plugin-nft-generation/src/api.ts @@ -2,7 +2,7 @@ import express from "express"; import { AgentRuntime } from "@ai16z/eliza"; import { createCollection } from "./handlers/createCollection.ts"; -import { createNFT } from "./handlers/createNFT.ts"; +import { createNFT, createNFTMetadata } from "./handlers/createNFT.ts"; import { verifyNFT } from "./handlers/verifyNFT.ts"; export function createNFTApiRouter(agents: Map) { @@ -39,6 +39,48 @@ export function createNFTApiRouter(agents: Map) { } ); + + router.post( + "/api/nft-generation/create-nft-metadata", + async (req: express.Request, res: express.Response) => { + const agentId = req.body.agentId; + const collectionName = req.body.collectionName; + const collectionAddress = req.body.collectionAddress; + const collectionAdminPublicKey = req.body.collectionAdminPublicKey; + const collectionFee = req.body.collectionFee; + const tokenId = req.body.tokenId; + const runtime = agents.get(agentId); + if (!runtime) { + res.status(404).send("Agent not found"); + return; + } + + try { + const nftInfo = await createNFTMetadata({ + runtime, + collectionName, + collectionAdminPublicKey, + collectionFee, + tokenId, + }); + + res.json({ + success: true, + data: { + ...nftInfo, + collectionAddress, + }, + }); + } catch (e: any) { + console.log(e); + res.json({ + success: false, + data: JSON.stringify(e), + }); + } + } + ); + router.post( "/api/nft-generation/create-nft", async (req: express.Request, res: express.Response) => { diff --git a/packages/plugin-nft-generation/src/handlers/createNFT.ts b/packages/plugin-nft-generation/src/handlers/createNFT.ts index 5ab5321b23b..281b444b168 100644 --- a/packages/plugin-nft-generation/src/handlers/createNFT.ts +++ b/packages/plugin-nft-generation/src/handlers/createNFT.ts @@ -126,7 +126,10 @@ export async function createNFTMetadata({ ); nftInfo.uri = jsonFilePath.url; - return nftInfo; + return { + ...nftInfo, + imageUri: nftImage.url + }; } return null; } From edcf0fadccc645f36ffcd8850059d430bc65f610 Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 13 Dec 2024 10:45:57 +0800 Subject: [PATCH 4/5] feat: Make revisions according to review comments. https://github.com/ai16z/eliza/pull/1011#discussion_r1883158466 https://github.com/ai16z/eliza/pull/1011#discussion_r1883142951 https://github.com/ai16z/eliza/pull/1011#discussion_r1883145780 --- agent/src/index.ts | 4 +- .../src/provider/wallet/walletSolana.ts | 257 ++++++++++-------- 2 files changed, 145 insertions(+), 116 deletions(-) diff --git a/agent/src/index.ts b/agent/src/index.ts index 5b1d1ac6c41..db8ab864d02 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -422,7 +422,9 @@ export async function createAgent( (getSecret(character, "SOLANA_PUBLIC_KEY") || (getSecret(character, "WALLET_PUBLIC_KEY") && !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))) && - getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") + getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") && + getSecret(character, "SOLANA_PRIVATE_KEY") && + getSecret(character, "SOLANA_ADMIN_PRIVATE_KEY") ? nftGenerationPlugin : null, getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, diff --git a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts index 6205b2aecea..763aecc3f33 100644 --- a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -79,46 +79,55 @@ export class WalletSolana { adminPublicKey: string; uri: string; fee: number; - }): Promise { - const collectionMint = generateSigner(this.umi); - elizaLogger.log("collectionMint", collectionMint, percentAmount(5)); - let transaction = new TransactionBuilder(); - const info = { - name, - symbol, - uri, - }; - console.log(`Metadata uploaded: ${uri}`); - transaction = transaction.add( - createNft(this.umi, { - ...info, - mint: collectionMint, - sellerFeeBasisPoints: percentAmount(fee), - isCollection: true, - }) - ); + }): Promise<{ + success: boolean; + link: string; + address: string; + error?: string | null; + }> { + try { + const collectionMint = generateSigner(this.umi); + let transaction = new TransactionBuilder(); + const info = { + name, + symbol, + uri, + }; + transaction = transaction.add( + createNft(this.umi, { + ...info, + mint: collectionMint, + sellerFeeBasisPoints: percentAmount(fee), + isCollection: true, + }) + ); - transaction = transaction.add( - updateV1(this.umi, { - mint: collectionMint.publicKey, - newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key - }) - ); + transaction = transaction.add( + updateV1(this.umi, { + mint: collectionMint.publicKey, + newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key + }) + ); - await transaction.sendAndConfirm(this.umi, { - confirm: {}, - }); + await transaction.sendAndConfirm(this.umi, { + confirm: {}, + }); - // await sleep(5000); - // const createdCollectionNft = await fetchDigitalAsset( - // this.umi, - // collectionMint.publicKey - // ); - const address = collectionMint.publicKey; - return { - link: getExplorerLink("address", address, this.cluster), - address, - }; + const address = collectionMint.publicKey; + return { + success: true, + link: getExplorerLink("address", address, this.cluster), + address, + error: null, + }; + } catch (e) { + return { + success: false, + link: "", + address: "", + error: e.message, + }; + } } async mintNFT({ @@ -135,63 +144,69 @@ export class WalletSolana { symbol: string; uri: string; fee: number; - }): Promise { - const umi = this.umi; - const mint = generateSigner(umi); - - let transaction = new TransactionBuilder(); - console.log("collectionAddress", collectionAddress); - const collectionAddressKey = publicKey(collectionAddress); - // Add SOL transfer instruction (0.1 SOL) - const receiverAddressKey = publicKey(adminPublicKey); // Replace with actual receiver address - transaction = transaction.add( - transferSol(umi, { - source: umi.identity, - destination: receiverAddressKey, - amount: sol(0.1), - }) - ); - const info = { - name, - uri, - symbol, - }; - transaction = transaction.add( - createNft(umi, { - mint, - ...info, - sellerFeeBasisPoints: percentAmount(fee), - collection: { - key: collectionAddressKey, - verified: false, - }, - }) - ); + }): Promise<{ + success: boolean; + link: string; + address: string; + error?: string | null; + }> { + try { + const umi = this.umi; + const mint = generateSigner(umi); - transaction = transaction.add( - updateV1(umi, { - mint: mint.publicKey, - newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key - }) - ); + let transaction = new TransactionBuilder(); + elizaLogger.log("collection address", collectionAddress); + const collectionAddressKey = publicKey(collectionAddress); + // Add SOL transfer instruction (0.1 SOL) + const receiverAddressKey = publicKey(adminPublicKey); // Replace with actual receiver address + transaction = transaction.add( + transferSol(umi, { + source: umi.identity, + destination: receiverAddressKey, + amount: sol(0.1), + }) + ); + const info = { + name, + uri, + symbol, + }; + transaction = transaction.add( + createNft(umi, { + mint, + ...info, + sellerFeeBasisPoints: percentAmount(fee), + collection: { + key: collectionAddressKey, + verified: false, + }, + }) + ); - await transaction.sendAndConfirm(umi); - // - // await sleep(); - // const createdNft = await fetchDigitalAsset(umi, mint.publicKey); - // - // console.log( - // `🖼️ Created NFT! Address is ${getExplorerLink( - // "address", - // createdNft.mint.publicKey, - // "devnet" - // )}` - // ); - const address = mint.publicKey; - return { - link: getExplorerLink("address", address, this.cluster), - address, - }; + transaction = transaction.add( + updateV1(umi, { + mint: mint.publicKey, + newUpdateAuthority: publicKey(adminPublicKey), // updateAuthority's public key + }) + ); + + await transaction.sendAndConfirm(umi); + + const address = mint.publicKey; + return { + success: true, + link: getExplorerLink("address", address, this.cluster), + address, + error: null, + }; + } catch (e) { + return { + success: false, + link: "", + address: "", + error: e.message, + }; + } } async verifyNft({ @@ -200,31 +215,43 @@ export class WalletSolana { }: { collectionAddress: string; nftAddress: string; - }) { - const umi = this.umi; - const collectionAddressKey = publicKey(collectionAddress); - const nftAddressKey = publicKey(nftAddress); - console.log("collectionAddress", collectionAddress); - console.log("nftAddress", nftAddress); - - let transaction = new TransactionBuilder(); - transaction = transaction.add( - verifyCollectionV1(umi, { - metadata: findMetadataPda(umi, { mint: nftAddressKey }), - collectionMint: collectionAddressKey, - authority: umi.identity, - }) - ); + }): Promise<{ + isVerified: boolean; + error: string | null; + }> { + try { + const umi = this.umi; + const collectionAddressKey = publicKey(collectionAddress); + const nftAddressKey = publicKey(nftAddress); - await transaction.sendAndConfirm(umi); + let transaction = new TransactionBuilder(); + transaction = transaction.add( + verifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: nftAddressKey }), + collectionMint: collectionAddressKey, + authority: umi.identity, + }) + ); - console.log( - `✅ NFT ${nftAddress} verified as member of collection ${collectionAddress}! See Explorer at ${getExplorerLink( - "address", - nftAddress, - this.cluster - )}` - ); + await transaction.sendAndConfirm(umi); + + elizaLogger.log( + `✅ NFT ${nftAddress} verified as member of collection ${collectionAddress}! See Explorer at ${getExplorerLink( + "address", + nftAddress, + this.cluster + )}` + ); + return { + isVerified: true, + error: null, + }; + } catch (e) { + return { + isVerified: false, + error: e.message, + }; + } } } From e9018e6c3f52775bd6825f7e6aadf136458e28ea Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 13 Dec 2024 11:11:30 +0800 Subject: [PATCH 5/5] feat: Remove the code used for debugging NFT minting at a price of 0.1 SOL sent to SOLANA_ADMIN_PUBLIC_KEY --- .../src/provider/wallet/walletSolana.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts index 763aecc3f33..98b2bee2330 100644 --- a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -157,15 +157,7 @@ export class WalletSolana { let transaction = new TransactionBuilder(); elizaLogger.log("collection address", collectionAddress); const collectionAddressKey = publicKey(collectionAddress); - // Add SOL transfer instruction (0.1 SOL) - const receiverAddressKey = publicKey(adminPublicKey); // Replace with actual receiver address - transaction = transaction.add( - transferSol(umi, { - source: umi.identity, - destination: receiverAddressKey, - amount: sol(0.1), - }) - ); + const info = { name, uri,