Skip to content

Commit 0bef570

Browse files
author
Mihail Kirov
committed
feat: nft actions
1 parent 907def2 commit 0bef570

11 files changed

+1603
-9
lines changed

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,10 @@ TEE_MARLIN_ATTESTATION_ENDPOINT= # Optional, default "http://127.0.0.1:1350"
570570
TON_PRIVATE_KEY= # Ton Mnemonic Seed Phrase Join With Empty String
571571
TON_RPC_URL= # ton rpc
572572
TON_RPC_API_KEY= # ton rpc api key
573+
TON_NFT_IMAGES_FOLDER= # Path to the folder containing the NFT images
574+
TON_NFT_METADATA_FOLDER= # Path to the folder containing the NFT metadata
575+
PINATA_API_KEY= # Pinata API key
576+
PINATA_API_SECRET= # Pinata API secret
573577

574578
# Sui
575579
SUI_PRIVATE_KEY= # Sui Mnemonic Seed Phrase (`sui keytool generate ed25519`) , Also support `suiprivatekeyxxxx` (sui keytool export --key-identity 0x63)

.gitignore

+7-1
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,10 @@ lit-config.json
9191

9292
# Configuration to exclude the extra and local_docs directories
9393
extra
94-
**/dist/**
94+
**/dist/**
95+
96+
ton_nft_metadata/
97+
ton_nft_metadata/*
98+
99+
ton_nft_images/
100+
ton_nft_images/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import {
2+
elizaLogger,
3+
composeContext,
4+
generateObject,
5+
ModelClass,
6+
type IAgentRuntime,
7+
type Memory,
8+
type State,
9+
type HandlerCallback,
10+
Content,
11+
} from "@elizaos/core";
12+
import {
13+
Address,
14+
Dictionary,
15+
} from "@ton/core";
16+
import { z } from "zod";
17+
import { initWalletProvider, type WalletProvider } from "../providers/wallet";
18+
19+
export interface GetCollectionDataContent extends Content {
20+
collectionAddress: string;
21+
}
22+
23+
function isGetCollectionDataContent(content: Content): content is GetCollectionDataContent {
24+
return typeof content.collectionAddress === "string";
25+
}
26+
27+
/**
28+
* Schema for retrieving NFT collection data.
29+
* - collectionAddress: the NFT collection smart contract address.
30+
* - endpoint: optional TON RPC endpoint, defaults to testnet if not specified.
31+
*/
32+
const getCollectionDataSchema = z.object({
33+
collectionAddress: z.string().nonempty("Collection address is required"),
34+
});
35+
36+
/**
37+
* Template guiding the extraction of collection data parameters.
38+
* The output should be a JSON markdown block similar to:
39+
*
40+
* {
41+
* "collectionAddress": "EQSomeCollectionAddressExample",
42+
* "endpoint": "https://testnet.toncenter.com/api/v2/jsonRPC"
43+
* }
44+
*/
45+
const getCollectionDataTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
46+
47+
Example response:
48+
\`\`\`json
49+
"collectionAddress": "EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4",
50+
}
51+
52+
{{recentMessages}}
53+
54+
Given the recent messages, extract the following information about the requested NFT collection data:
55+
- Collection address
56+
57+
Respond with a JSON markdown block containing only the extracted values.`;
58+
59+
60+
/**
61+
* GetCollectionDataAction encapsulates the core logic to retrieve NFT collection data.
62+
*/
63+
class GetCollectionDataAction {
64+
private readonly walletProvider: WalletProvider;
65+
66+
constructor(walletProvider: WalletProvider) {
67+
this.walletProvider = walletProvider;
68+
}
69+
70+
/**
71+
* Retrieves and parses collection data from the provided collection address.
72+
* Returns an object containing the next NFT index, owner address and parsed NFT items.
73+
*/
74+
async getData(
75+
collectionAddress: string,
76+
): Promise<{
77+
collectionAddress: string;
78+
nextItemIndex: number;
79+
ownerAddress: string | null;
80+
nftItems: Array<{ index: number; deposit: string; ownerAddress: string; meta: string }>;
81+
message: string;
82+
}> {
83+
const walletClient = this.walletProvider.getWalletClient();
84+
85+
try {
86+
const addr = Address.parse(collectionAddress);
87+
88+
// Run the get_collection_data method on the collection contract.
89+
// Per TEP-62, it returns:
90+
// (int next_item_index, cell collection_content, slice owner_address).
91+
const result = await walletClient.runMethod(addr, "get_collection_data");
92+
93+
// Extract the next NFT index.
94+
const nextItemIndex = result.stack.readNumber();
95+
96+
const nftItems = [];
97+
for(let i = 0; i < nextItemIndex; i++) {
98+
const item = await walletClient.runMethod(addr, "get_nft_item",
99+
[{ type: "int", value: BigInt(i) }]);
100+
nftItems.push(item);
101+
}
102+
let ownerAddressStr: string | null = null;
103+
try {
104+
const ownerAddress = result.stack.readAddress();
105+
ownerAddressStr = ownerAddress.toString();
106+
} catch (e) {
107+
ownerAddressStr = null;
108+
}
109+
110+
return {
111+
collectionAddress,
112+
nextItemIndex,
113+
ownerAddress: ownerAddressStr,
114+
nftItems,
115+
message: "Collection data fetched successfully",
116+
};
117+
} catch (error: any) {
118+
elizaLogger.error("Error fetching collection data:", error);
119+
throw error;
120+
}
121+
}
122+
}
123+
124+
/**
125+
* Helper function that builds collection data details.
126+
*/
127+
const buildGetCollectionData = async (
128+
runtime: IAgentRuntime,
129+
message: Memory,
130+
state: State
131+
): Promise<GetCollectionDataContent> => {
132+
133+
// Initialize or update state
134+
let currentState = state;
135+
if (!currentState) {
136+
currentState = (await runtime.composeState(message)) as State;
137+
} else {
138+
currentState = await runtime.updateRecentMessageState(currentState);
139+
}
140+
141+
const getCollectionContext = composeContext({
142+
state: currentState,
143+
template: getCollectionDataTemplate,
144+
});
145+
const content = await generateObject({
146+
runtime,
147+
context: getCollectionContext,
148+
schema: getCollectionDataSchema,
149+
modelClass: ModelClass.SMALL,
150+
});
151+
152+
let buildGetCollectionDataContent: GetCollectionDataContent = content.object as GetCollectionDataContent;
153+
154+
if (buildGetCollectionDataContent === undefined) {
155+
buildGetCollectionDataContent = content as unknown as GetCollectionDataContent;
156+
}
157+
158+
return buildGetCollectionDataContent;
159+
};
160+
161+
export default {
162+
name: "GET_NFT_COLLECTION_DATA",
163+
similes: ["GET_COLLECTION_DATA", "FETCH_NFT_COLLECTION"],
164+
description:
165+
"Fetches collection data (next NFT index, array of NFT items, and owner address) from the provided NFT collection address.",
166+
handler: async (
167+
runtime: IAgentRuntime,
168+
message: Memory,
169+
state: State,
170+
_options: Record<string, unknown>,
171+
callback?: HandlerCallback
172+
) => {
173+
elizaLogger.log("Starting GET_NFT_COLLECTION_DATA handler...");
174+
// Build collection data details using the helper method.
175+
const getCollectionDetails = await buildGetCollectionData(runtime, message, state);
176+
177+
if(!isGetCollectionDataContent(getCollectionDetails)) {
178+
if(callback) {
179+
callback({
180+
text: "Unable to process get collection data request. Invalid content provided.",
181+
content: { error: "Invalid get collection data content" },
182+
});
183+
}
184+
return false;
185+
}
186+
187+
try {
188+
const walletProvider = await initWalletProvider(runtime);
189+
const getCollectionDataAction = new GetCollectionDataAction(walletProvider);
190+
const collectionData = await getCollectionDataAction.getData(getCollectionDetails.collectionAddress);
191+
192+
if (callback) {
193+
callback({
194+
text: JSON.stringify(collectionData, null, 2),
195+
content: collectionData,
196+
});
197+
}
198+
return true;
199+
} catch (error: any) {
200+
elizaLogger.error("Error fetching collection data:", error);
201+
if (callback) {
202+
callback({
203+
text: `Error fetching collection data: ${error.message}`,
204+
content: { error: error.message },
205+
});
206+
}
207+
return false;
208+
}
209+
},
210+
validate: async (_runtime: IAgentRuntime) => true,
211+
examples: [
212+
[
213+
{
214+
user: "{{user1}}",
215+
content: {
216+
collectionAddress: "EQSomeCollectionAddressExample",
217+
action: "GET_NFT_COLLECTION_DATA",
218+
},
219+
},
220+
{
221+
user: "{{user1}}",
222+
content: {
223+
text: "Collection data fetched successfully. Next index: ..., NFT items: [...]",
224+
},
225+
},
226+
],
227+
],
228+
};

0 commit comments

Comments
 (0)