Skip to content

Commit 6c78f1a

Browse files
committedDec 30, 2024·
feat: add massa-plugin
1 parent 0b3e2a2 commit 6c78f1a

14 files changed

+990
-159
lines changed
 

‎agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@elizaos/plugin-goat": "workspace:*",
4545
"@elizaos/plugin-icp": "workspace:*",
4646
"@elizaos/plugin-image-generation": "workspace:*",
47+
"@elizaos/plugin-massa": "workspace:*",
4748
"@elizaos/plugin-nft-generation": "workspace:*",
4849
"@elizaos/plugin-node": "workspace:*",
4950
"@elizaos/plugin-solana": "workspace:*",

‎packages/plugin-massa/.npmignore

+6
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
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import eslintGlobalConfig from "../../eslint.config.mjs";
2+
3+
export default [...eslintGlobalConfig];

‎packages/plugin-massa/package.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@elizaos/plugin-massa",
3+
"version": "0.0.1-alpha.1",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"dependencies": {
8+
"@elizaos/core": "workspace:*",
9+
"@massalabs/massa-web3": "^5.0.1-dev",
10+
"tsup": "8.3.5"
11+
},
12+
"scripts": {
13+
"build": "tsup --format esm --dts",
14+
"lint": "eslint . --fix"
15+
},
16+
"peerDependencies": {
17+
"whatwg-url": "7.1.0"
18+
}
19+
}

‎packages/plugin-massa/readme.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Massa Plugin
2+
3+
## Overview
4+
5+
This plugin aims to be the basis of all interactions with the Massa ecosystem.
6+
7+
## Adding a new action
8+
9+
Reuse providers and utilities from the existing actions where possible. Add more utilities if you think they will be useful for other actions.
10+
11+
1. Add the action to the `actions` directory. Try to follow the naming convention of the other actions.
12+
2. Export the action in the `index.ts` file.
13+
14+
15+
## MASSA documentation
16+
[https://docs.massa.net/](https://docs.massa.net/)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
// It should transfer tokens from the agent's wallet to the recipient.
2+
3+
import {
4+
type Action,
5+
ActionExample,
6+
composeContext,
7+
Content,
8+
elizaLogger,
9+
generateObjectDeprecated,
10+
HandlerCallback,
11+
IAgentRuntime,
12+
Memory,
13+
ModelClass,
14+
State,
15+
} from "@elizaos/core";
16+
import { validateConfig } from "../enviroment";
17+
import { getMnsTarget } from "../utils/mns";
18+
import {
19+
Web3Provider,
20+
Account,
21+
Address,
22+
MRC20,
23+
MAINNET_TOKENS,
24+
parseUnits,
25+
CHAIN_ID,
26+
BUILDNET_TOKENS,
27+
} from "@massalabs/massa-web3";
28+
import { validateAddress } from "../utils/address";
29+
30+
export interface TransferContent extends Content {
31+
tokenAddress: string;
32+
recipient: string;
33+
amount: string;
34+
}
35+
36+
export function isTransferContent(content: any): content is TransferContent {
37+
elizaLogger.log("Starting SEND_TOKEN content", content);
38+
39+
// Validate types
40+
const validTypes =
41+
typeof content.tokenAddress === "string" &&
42+
typeof content.recipient === "string" &&
43+
(typeof content.amount === "string" ||
44+
typeof content.amount === "number");
45+
46+
if (!validTypes) {
47+
return false;
48+
}
49+
50+
const tokenAddr = validateAddress(content.tokenAddress);
51+
if (!tokenAddr || tokenAddr.isEOA) {
52+
return false;
53+
}
54+
55+
const recipient: string = content.recipient;
56+
// Additional checks based on whether recipient or mns is defined
57+
if (recipient && !recipient.endsWith(".massa")) {
58+
Address.fromString(content.recipient);
59+
}
60+
61+
return true;
62+
}
63+
64+
const transferTemplate = (
65+
tokens: Record<string, string>
66+
) => `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
67+
68+
Smart contrat addresses are prefixed with "AS" and EOA addresses used for recipient are prefixed with "AU".
69+
70+
These are known token addresses, if you get asked about them, use these:
71+
${Object.entries(tokens)
72+
.map(([name, address]) => `- ${name}: ${address}`)
73+
.join("\n")}
74+
75+
If a EOA recipient address is provided, use it as is. If a .massa name is provided, use it as recipient.
76+
77+
Example response:
78+
\`\`\`json
79+
{
80+
"tokenAddress": "AS12LpYyAjYRJfYhyu7fkrS224gMdvFHVEeVWoeHZzMdhis7UZ3Eb",
81+
"recipient": "mymassaname.massa",
82+
"amount": "0.001"
83+
}
84+
\`\`\`
85+
86+
{{recentMessages}}
87+
88+
Given the recent messages, extract the following information about the requested token transfer:
89+
- Amount in string format
90+
- Token contract address
91+
- Recipient wallet address or .massa name
92+
93+
If one of the values cannot be determined, ask user for missing information.
94+
95+
96+
Respond with a JSON markdown block containing only the extracted values.`;
97+
98+
export default {
99+
name: "SEND_TOKEN",
100+
similes: [
101+
"TRANSFER_TOKEN_ON_MASSA",
102+
"TRANSFER_TOKENS_ON_MASSA",
103+
"SEND_TOKENS_ON_MASSA",
104+
"SEND_ETH_ON_MASSA",
105+
"PAY_ON_MASSA",
106+
],
107+
validate: async (runtime: IAgentRuntime, _message: Memory) => {
108+
await validateConfig(runtime);
109+
return true;
110+
},
111+
description:
112+
"MUST use this action if the user requests send a token or transfer a token, the request might be varied, but it will always be a token transfer.",
113+
handler: async (
114+
runtime: IAgentRuntime,
115+
message: Memory,
116+
state: State,
117+
_options: { [key: string]: unknown },
118+
callback?: HandlerCallback
119+
): Promise<boolean> => {
120+
elizaLogger.log("Starting SEND_TOKEN handler...");
121+
122+
// Initialize or update state
123+
if (!state) {
124+
state = (await runtime.composeState(message)) as State;
125+
} else {
126+
state = await runtime.updateRecentMessageState(state);
127+
}
128+
129+
const secretKey = runtime.getSetting("MASSA_PRIVATE_KEY");
130+
if (!secretKey) {
131+
throw new Error("MASSA wallet credentials not configured");
132+
}
133+
const account = await Account.fromPrivateKey(secretKey);
134+
135+
const rpc = runtime.getSetting("MASSA_RPC_URL");
136+
if (!rpc) {
137+
throw new Error("MASSA_RPC_URL not configured");
138+
}
139+
const provider = Web3Provider.fromRPCUrl(rpc, account);
140+
141+
const { chainId } = await provider.networkInfos();
142+
// Compose transfer context
143+
const transferContext = composeContext({
144+
state,
145+
template: transferTemplate(
146+
chainId === CHAIN_ID.Mainnet ? MAINNET_TOKENS : BUILDNET_TOKENS
147+
),
148+
});
149+
150+
// Generate transfer content
151+
const content = await generateObjectDeprecated({
152+
runtime,
153+
context: transferContext,
154+
modelClass: ModelClass.MEDIUM,
155+
});
156+
157+
elizaLogger.debug("Transfer content:", content);
158+
159+
// Validate transfer content
160+
const isValid = isTransferContent(content);
161+
162+
if (!isValid) {
163+
elizaLogger.error("Invalid content for TRANSFER_TOKEN action.");
164+
if (callback) {
165+
callback({
166+
text: "Not enough information to transfer tokens. Please respond with token address, recipient address or massa name, and amount.",
167+
content: { error: "Invalid transfer content" },
168+
});
169+
}
170+
return false;
171+
}
172+
173+
let recipientAddress = content.recipient;
174+
// Validate recipient address
175+
if (content.recipient.endsWith(".massa")) {
176+
try {
177+
recipientAddress = await getMnsTarget(provider, content.recipient.substring(0, content.recipient.length - ".massa".length));
178+
Address.fromString(recipientAddress);
179+
} catch (error: any) {
180+
elizaLogger.error(
181+
"Error resolving MNS target:",
182+
error?.message
183+
);
184+
if (callback) {
185+
callback({
186+
text: `Error resolving MNS target: ${error?.message}`,
187+
content: { error: error },
188+
});
189+
}
190+
return false;
191+
}
192+
}
193+
194+
try {
195+
const mrc20Token = new MRC20(provider, content.tokenAddress);
196+
const decimals = await mrc20Token.decimals();
197+
const amount = parseUnits(content.amount, decimals);
198+
const operation = await mrc20Token.transfer(
199+
recipientAddress,
200+
amount
201+
);
202+
203+
elizaLogger.success(
204+
"Transferring",
205+
amount,
206+
"of",
207+
content.tokenAddress,
208+
"to",
209+
recipientAddress
210+
);
211+
212+
await operation.waitSpeculativeExecution();
213+
214+
elizaLogger.success(
215+
"Transfer completed successfully! Operation id: " + operation.id
216+
);
217+
if (callback) {
218+
callback({
219+
text: `Successfully transferred ${content.amount} tokens to ${content.recipient}\n OperationId: ${operation.id}`,
220+
content: {
221+
success: true,
222+
operationId: operation.id,
223+
amount: content.amount,
224+
token: content.tokenAddress,
225+
recipient: content.recipient,
226+
},
227+
});
228+
}
229+
230+
return true;
231+
} catch (error: any) {
232+
elizaLogger.error("Error during token transfer:", error?.message);
233+
if (callback) {
234+
callback({
235+
text: `Error transferring tokens: ${error?.message}`,
236+
content: { error: error },
237+
});
238+
}
239+
return false;
240+
}
241+
},
242+
243+
examples: [
244+
[
245+
{
246+
user: "{{user1}}",
247+
content: {
248+
text: "Send 10 WMAS to AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep",
249+
},
250+
},
251+
{
252+
user: "{{agent}}",
253+
content: {
254+
text: "I'll transfer 10 WMAS to that address right away. Let me process that for you.",
255+
},
256+
},
257+
{
258+
user: "{{agent}}",
259+
content: {
260+
text: "Successfully sent 10 WMAS tokens to AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep\n Operation id: O12fZa1oNL18s3ZV2PCXVYUmQz2cQrNqKfFaRsyJNFsAcGYxEAKD",
261+
},
262+
},
263+
],
264+
[
265+
{
266+
user: "{{user1}}",
267+
content: {
268+
text: "Send 10 DAI to domain.massa",
269+
},
270+
},
271+
{
272+
user: "{{agent}}",
273+
content: {
274+
text: "I'll transfer 10 DAI to domain.massa right away. Let me process that for you.",
275+
},
276+
},
277+
{
278+
user: "{{agent}}",
279+
content: {
280+
text: "Successfully sent 10 DAI tokens to AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep\n Operation id: O12fZa1oNL18s3ZV2PCXVYUmQz2cQrNqKfFaRsyJNFsAcGYxEAKD",
281+
},
282+
},
283+
],
284+
] as ActionExample[][],
285+
} as Action;
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { IAgentRuntime } from "@elizaos/core";
2+
import { PublicApiUrl } from "@massalabs/massa-web3";
3+
import { z } from "zod";
4+
5+
export const massaEnvSchema = z.object({
6+
MASSA_PRIVATE_KEY: z.string().min(1, "Massa private key is required"),
7+
MASSA_RPC_URL: z.string().min(1, "Massa RPC URL is required"),
8+
});
9+
10+
export type MassaConfig = z.infer<typeof massaEnvSchema>;
11+
12+
export async function validateConfig(
13+
runtime: IAgentRuntime
14+
): Promise<MassaConfig> {
15+
try {
16+
const config = {
17+
MASSA_PRIVATE_KEY:
18+
runtime.getSetting("MASSA_PRIVATE_KEY") ||
19+
process.env.MASSA_PRIVATE_KEY,
20+
MASSA_RPC_URL:
21+
runtime.getSetting("MASSA_RPC_URL") ||
22+
process.env.MASSA_RPC_URL ||
23+
PublicApiUrl.Mainnet,
24+
};
25+
26+
return massaEnvSchema.parse(config);
27+
} catch (error) {
28+
if (error instanceof z.ZodError) {
29+
const errorMessages = error.errors
30+
.map((err) => `${err.path.join(".")}: ${err.message}`)
31+
.join("\n");
32+
throw new Error(
33+
`Massa configuration validation failed:\n${errorMessages}`
34+
);
35+
}
36+
throw error;
37+
}
38+
}

‎packages/plugin-massa/src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Plugin } from "@elizaos/core";
2+
import transfer from "./actions/transfer";
3+
4+
export const massaPlugin: Plugin = {
5+
name: "massa",
6+
description: "Massa Plugin for Eliza",
7+
actions: [transfer],
8+
evaluators: [],
9+
providers: [],
10+
};
11+
12+
export default massaPlugin;

0 commit comments

Comments
 (0)
Please sign in to comment.