diff --git a/.env.example b/.env.example index 28ad9b7c2f7..e6c7dbd7bb5 100644 --- a/.env.example +++ b/.env.example @@ -153,6 +153,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 5017d73758c..93aae19487b 100644 --- a/agent/package.json +++ b/agent/package.json @@ -38,6 +38,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 6306af2ae35..d5f4a279ea6 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -48,6 +48,7 @@ import { solanaPlugin } from "@ai16z/plugin-solana"; import { TEEMode, teePlugin } from "@ai16z/plugin-tee"; import { tonPlugin } from "@ai16z/plugin-ton"; import { zksyncEraPlugin } from "@ai16z/plugin-zksync-era"; +import { nftGenerationPlugin, createNFTApiRouter } from "@ai16z/plugin-nft-generation"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -485,6 +486,14 @@ 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") && + getSecret(character, "SOLANA_PRIVATE_KEY") && + getSecret(character, "SOLANA_ADMIN_PRIVATE_KEY") + ? nftGenerationPlugin + : null, getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, getSecret(character, "COINBASE_COMMERCE_KEY") ? coinbaseCommercePlugin 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 b5baa2ec01c..2862f517ab6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -760,6 +760,10 @@ export type Character = { bio: string; nicknames?: string[]; }; + /** Optional NFT prompt */ + nft?: { + prompt: string; + } }; /** @@ -1156,6 +1160,7 @@ export interface IPdfService extends Service { export interface IAwsS3Service extends Service { uploadFile( imagePath: string, + subDirectory: string, useSignedUrl: boolean, expiresIn: number ): Promise<{ 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..83091cf8976 --- /dev/null +++ b/packages/plugin-nft-generation/src/api.ts @@ -0,0 +1,165 @@ +import express from "express"; + +import { AgentRuntime } from "@ai16z/eliza"; +import { createCollection } from "./handlers/createCollection.ts"; +import { createNFT, createNFTMetadata } 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-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) => { + 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..281b444b168 --- /dev/null +++ b/packages/plugin-nft-generation/src/handlers/createNFT.ts @@ -0,0 +1,182 @@ +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: `#${tokenId}`, + 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, + imageUri: nftImage.url + }; + } + 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, + symbol: nftInfo.symbol, + 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..98b2bee2330 --- /dev/null +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -0,0 +1,250 @@ +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<{ + 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 + }) + ); + + await transaction.sendAndConfirm(this.umi, { + confirm: {}, + }); + + 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({ + collectionAddress, + adminPublicKey, + name, + symbol, + uri, + fee, + }: { + collectionAddress: string; + adminPublicKey: string; + name: string; + symbol: string; + uri: string; + fee: number; + }): Promise<{ + success: boolean; + link: string; + address: string; + error?: string | null; + }> { + try { + const umi = this.umi; + const mint = generateSigner(umi); + + let transaction = new TransactionBuilder(); + elizaLogger.log("collection address", collectionAddress); + const collectionAddressKey = publicKey(collectionAddress); + + const info = { + name, + uri, + symbol, + }; + 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); + + 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({ + collectionAddress, + nftAddress, + }: { + collectionAddress: string; + nftAddress: string; + }): Promise<{ + isVerified: boolean; + error: string | null; + }> { + try { + const umi = this.umi; + const collectionAddressKey = publicKey(collectionAddress); + const nftAddressKey = publicKey(nftAddress); + + let transaction = new TransactionBuilder(); + transaction = transaction.add( + verifyCollectionV1(umi, { + metadata: findMetadataPda(umi, { mint: nftAddressKey }), + collectionMint: collectionAddressKey, + authority: umi.identity, + }) + ); + + 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, + }; + } + } +} + +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/turbo.json b/turbo.json index 2ae8ce02b7e..f3fc60e2bac 100644 --- a/turbo.json +++ b/turbo.json @@ -18,6 +18,9 @@ "outputs": ["dist/**"], "dependsOn": ["@ai16z/plugin-trustdb#build"] }, + "@ai16z/plugin-nft-generation#build": { + "dependsOn": ["@ai16z/plugin-node#build"] + }, "eliza-docs#build": { "outputs": ["build/**"] },