Skip to content

Commit 3ff0677

Browse files
committed
feat: Add plugin-nft-generation: support for creating Solana NFT collections.
1 parent 9d1a131 commit 3ff0677

19 files changed

+1238
-4
lines changed

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ EVM_PROVIDER_URL=
147147
# Solana
148148
SOLANA_PRIVATE_KEY=
149149
SOLANA_PUBLIC_KEY=
150+
SOLANA_CLUSTER= # Default: devnet. Solana Cluster: 'devnet' | 'testnet' | 'mainnet-beta'
151+
SOLANA_ADMIN_PRIVATE_KEY= # This wallet is used to verify NFTs
152+
SOLANA_ADMIN_PUBLIC_KEY= # This wallet is used to verify NFTs
153+
SOLANA_VERIFY_TOKEN= # Authentication token for calling the verification API
150154

151155
# Fallback Wallet Configuration (deprecated)
152156
WALLET_PRIVATE_KEY=

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@ai16z/plugin-goat": "workspace:*",
3838
"@ai16z/plugin-icp": "workspace:*",
3939
"@ai16z/plugin-image-generation": "workspace:*",
40+
"@ai16z/plugin-nft-generation": "workspace:*",
4041
"@ai16z/plugin-node": "workspace:*",
4142
"@ai16z/plugin-solana": "workspace:*",
4243
"@ai16z/plugin-starknet": "workspace:*",

agent/src/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { solanaPlugin } from "@ai16z/plugin-solana";
4444
import { teePlugin, TEEMode } from "@ai16z/plugin-tee";
4545
import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos";
4646
import { flowPlugin } from "@ai16z/plugin-flow";
47+
import { nftGenerationPlugin, createNFTApiRouter } from "@ai16z/plugin-nft-generation";
4748
import Database from "better-sqlite3";
4849
import fs from "fs";
4950
import path from "path";
@@ -418,6 +419,12 @@ export async function createAgent(
418419
getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))
419420
? evmPlugin
420421
: null,
422+
(getSecret(character, "SOLANA_PUBLIC_KEY") ||
423+
(getSecret(character, "WALLET_PUBLIC_KEY") &&
424+
!getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))) &&
425+
getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY")
426+
? nftGenerationPlugin
427+
: null,
421428
getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null,
422429
getSecret(character, "COINBASE_COMMERCE_KEY")
423430
? coinbaseCommercePlugin
@@ -498,6 +505,12 @@ async function startAgent(character: Character, directClient) {
498505

499506
directClient.registerAgent(runtime);
500507

508+
// Support using API to create NFT
509+
// const agents = new Map();
510+
// agents.set(runtime.agentId, runtime)
511+
// const apiNFTGenerationRouter = createNFTApiRouter(agents);
512+
// directClient?.app?.use(apiNFTGenerationRouter)
513+
501514
return clients;
502515
} catch (error) {
503516
elizaLogger.error(

packages/core/src/environment.ts

+5
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ export const CharacterSchema = z.object({
124124
nicknames: z.array(z.string()).optional(),
125125
})
126126
.optional(),
127+
nft: z
128+
.object({
129+
prompt: z.string().optional(),
130+
})
131+
.optional(),
127132
});
128133

129134
// Type inference

packages/core/src/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,10 @@ export type Character = {
735735
bio: string;
736736
nicknames?: string[];
737737
};
738+
/** Optional NFT prompt */
739+
nft?: {
740+
prompt: string;
741+
}
738742
};
739743

740744
/**
@@ -1124,7 +1128,7 @@ export interface IPdfService extends Service {
11241128
}
11251129

11261130
export interface IAwsS3Service extends Service {
1127-
uploadFile(imagePath: string, useSignedUrl: boolean, expiresIn: number ): Promise<{
1131+
uploadFile(imagePath: string, subDirectory: string, useSignedUrl: boolean, expiresIn: number ): Promise<{
11281132
success: boolean;
11291133
url?: string;
11301134
error?: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
3+
!dist/**
4+
!package.json
5+
!readme.md
6+
!tsup.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import eslintGlobalConfig from "../../eslint.config.mjs";
2+
3+
export default [...eslintGlobalConfig];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@ai16z/plugin-nft-generation",
3+
"version": "0.1.5-alpha.5",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"dependencies": {
8+
"@ai16z/eliza": "workspace:*",
9+
"@ai16z/plugin-image-generation": "workspace:*",
10+
"@ai16z/plugin-node": "workspace:*",
11+
"@metaplex-foundation/mpl-token-metadata": "^3.3.0",
12+
"@metaplex-foundation/mpl-toolbox": "^0.9.4",
13+
"@metaplex-foundation/umi": "^0.9.2",
14+
"@metaplex-foundation/umi-bundle-defaults": "^0.9.2",
15+
"@solana-developers/helpers": "^2.5.6",
16+
"@solana/web3.js": "1.95.5",
17+
"bs58": "6.0.0",
18+
"express": "4.21.1",
19+
"node-cache": "5.1.2",
20+
"tsup": "8.3.5"
21+
},
22+
"scripts": {
23+
"build": "tsup --format esm --dts",
24+
"dev": "tsup --format esm --dts --watch",
25+
"lint": "eslint . --fix"
26+
},
27+
"peerDependencies": {
28+
"whatwg-url": "7.1.0"
29+
}
30+
}
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import express from "express";
2+
3+
import { AgentRuntime } from "@ai16z/eliza";
4+
import { createCollection } from "./handlers/createCollection.ts";
5+
import { createNFT } from "./handlers/createNFT.ts";
6+
import { verifyNFT } from "./handlers/verifyNFT.ts";
7+
8+
export function createNFTApiRouter(agents: Map<string, AgentRuntime>) {
9+
const router = express.Router();
10+
11+
router.post(
12+
"/api/nft-generation/create-collection",
13+
async (req: express.Request, res: express.Response) => {
14+
const agentId = req.body.agentId;
15+
const fee = req.body.fee || 0;
16+
const runtime = agents.get(agentId);
17+
if (!runtime) {
18+
res.status(404).send("Agent not found");
19+
return;
20+
}
21+
try {
22+
const collectionAddressRes = await createCollection({
23+
runtime,
24+
collectionName: runtime.character.name,
25+
fee
26+
});
27+
28+
res.json({
29+
success: true,
30+
data: collectionAddressRes,
31+
});
32+
} catch (e: any) {
33+
console.log(e);
34+
res.json({
35+
success: false,
36+
data: JSON.stringify(e),
37+
});
38+
}
39+
}
40+
);
41+
42+
router.post(
43+
"/api/nft-generation/create-nft",
44+
async (req: express.Request, res: express.Response) => {
45+
const agentId = req.body.agentId;
46+
const collectionName = req.body.collectionName;
47+
const collectionAddress = req.body.collectionAddress;
48+
const collectionAdminPublicKey = req.body.collectionAdminPublicKey;
49+
const collectionFee = req.body.collectionFee;
50+
const tokenId = req.body.tokenId;
51+
const runtime = agents.get(agentId);
52+
if (!runtime) {
53+
res.status(404).send("Agent not found");
54+
return;
55+
}
56+
57+
try {
58+
const nftRes = await createNFT({
59+
runtime,
60+
collectionName,
61+
collectionAddress,
62+
collectionAdminPublicKey,
63+
collectionFee,
64+
tokenId,
65+
});
66+
67+
res.json({
68+
success: true,
69+
data: nftRes,
70+
});
71+
} catch (e: any) {
72+
console.log(e);
73+
res.json({
74+
success: false,
75+
data: JSON.stringify(e),
76+
});
77+
}
78+
}
79+
);
80+
81+
82+
router.post(
83+
"/api/nft-generation/verify-nft",
84+
async (req: express.Request, res: express.Response) => {
85+
const agentId = req.body.agentId;
86+
const collectionAddress = req.body.collectionAddress;
87+
const NFTAddress = req.body.nftAddress;
88+
const token = req.body.token;
89+
90+
const runtime = agents.get(agentId);
91+
if (!runtime) {
92+
res.status(404).send("Agent not found");
93+
return;
94+
}
95+
const verifyToken = runtime.getSetting('SOLANA_VERIFY_TOKEN')
96+
if (token !== verifyToken) {
97+
res.status(401).send(" Access denied for translation");
98+
return;
99+
}
100+
try {
101+
const {success} = await verifyNFT({
102+
runtime,
103+
collectionAddress,
104+
NFTAddress,
105+
});
106+
107+
res.json({
108+
success: true,
109+
data: success ? 'verified' : 'unverified',
110+
});
111+
} catch (e: any) {
112+
console.log(e);
113+
res.json({
114+
success: false,
115+
data: JSON.stringify(e),
116+
});
117+
}
118+
}
119+
);
120+
121+
122+
return router;
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { AwsS3Service } from "@ai16z/plugin-node";
2+
import {
3+
composeContext,
4+
elizaLogger,
5+
generateImage,
6+
getEmbeddingZeroVector,
7+
IAgentRuntime,
8+
Memory,
9+
ServiceType,
10+
stringToUuid,
11+
} from "@ai16z/eliza";
12+
import {
13+
saveBase64Image,
14+
saveHeuristImage,
15+
} from "@ai16z/plugin-image-generation";
16+
import { PublicKey } from "@solana/web3.js";
17+
import WalletSolana from "../provider/wallet/walletSolana.ts";
18+
19+
const collectionImageTemplate = `
20+
Generate a logo with the text "{{collectionName}}", using orange as the main color, with a sci-fi and mysterious background theme
21+
`;
22+
23+
export async function createCollection({
24+
runtime,
25+
collectionName,
26+
fee,
27+
}: {
28+
runtime: IAgentRuntime;
29+
collectionName: string;
30+
fee?: number;
31+
}) {
32+
const userId = runtime.agentId;
33+
elizaLogger.log("User ID:", userId);
34+
const awsS3Service: AwsS3Service = runtime.getService(ServiceType.AWS_S3);
35+
const agentName = runtime.character.name;
36+
const roomId = stringToUuid("nft_generate_room-" + agentName);
37+
// Create memory for the message
38+
const memory: Memory = {
39+
agentId: userId,
40+
userId,
41+
roomId,
42+
content: {
43+
text: "",
44+
45+
source: "nft-generator",
46+
},
47+
createdAt: Date.now(),
48+
embedding: getEmbeddingZeroVector(),
49+
};
50+
const state = await runtime.composeState(memory, {
51+
collectionName,
52+
});
53+
54+
const prompt = composeContext({
55+
state,
56+
template: collectionImageTemplate,
57+
});
58+
const images = await generateImage(
59+
{
60+
prompt,
61+
width: 300,
62+
height: 300,
63+
},
64+
runtime
65+
);
66+
if (images.success && images.data && images.data.length > 0) {
67+
const image = images.data[0];
68+
const filename = `collection-image`;
69+
if (image.startsWith("http")) {
70+
elizaLogger.log("Generating image url:", image);
71+
}
72+
// Choose save function based on image data format
73+
const filepath = image.startsWith("http")
74+
? await saveHeuristImage(image, filename)
75+
: saveBase64Image(image, filename);
76+
77+
const logoPath = await awsS3Service.uploadFile(
78+
filepath,
79+
`/${collectionName}`,
80+
false
81+
);
82+
const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY");
83+
const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY");
84+
const adminPublicKey = runtime.getSetting("SOLANA_ADMIN_PUBLIC_KEY");
85+
const collectionInfo = {
86+
name: `${collectionName}`,
87+
symbol: `${collectionName.toUpperCase()[0]}`,
88+
adminPublicKey,
89+
fee: fee || 0,
90+
uri: "",
91+
};
92+
const jsonFilePath = await awsS3Service.uploadJson(
93+
{
94+
name: collectionInfo.name,
95+
description: `${collectionInfo.name}`,
96+
image: logoPath.url,
97+
},
98+
"metadata.json",
99+
`${collectionName}`
100+
);
101+
collectionInfo.uri = jsonFilePath.url;
102+
103+
const wallet = new WalletSolana(new PublicKey(publicKey), privateKey);
104+
105+
const collectionAddressRes = await wallet.createCollection({
106+
...collectionInfo,
107+
});
108+
109+
return {
110+
network: "solana",
111+
address: collectionAddressRes.address,
112+
link: collectionAddressRes.link,
113+
collectionInfo,
114+
};
115+
}
116+
117+
return;
118+
}

0 commit comments

Comments
 (0)