Skip to content

Commit 30b22ab

Browse files
mgavrilawtfsayo
andauthored
feat: Add swap & improvements for multiversx-plugin (#2651)
* Add balance check for transactions * Revert index changes * Add swap functionality * Remove leftover log * Update --------- Co-authored-by: Sayo <hi@sayo.wtf>
1 parent 1f46b48 commit 30b22ab

15 files changed

+729
-92
lines changed

packages/plugin-multiversx/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
"dependencies": {
2222
"@elizaos/core": "workspace:*",
2323
"@multiversx/sdk-core": "13.15.0",
24+
"@multiversx/sdk-native-auth-client": "1.0.9",
2425
"bignumber.js": "9.1.2",
2526
"browserify": "^17.0.1",
2627
"esbuild-plugin-polyfill-node": "^0.3.0",
2728
"esmify": "^2.1.1",
2829
"tsup": "8.3.5",
29-
"vitest": "2.1.5"
30+
"vitest": "2.1.5",
31+
"graphql-request": "7.1.2"
3032
},
3133
"scripts": {
3234
"build": "tsup --format esm --dts",

packages/plugin-multiversx/src/actions/createToken.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@ export interface CreateTokenContent extends Content {
2121
amount: string;
2222
}
2323

24-
function isCreateTokenContent(
25-
runtime: IAgentRuntime,
26-
content: CreateTokenContent
27-
) {
28-
console.log("Content for create token", content);
29-
return content.tokenName && content.tokenName && content.tokenName;
30-
}
31-
3224
const createTokenTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
3325
3426
Example response:
@@ -65,7 +57,7 @@ export default {
6557
message: Memory,
6658
state: State,
6759
_options: { [key: string]: unknown },
68-
callback?: HandlerCallback
60+
callback?: HandlerCallback,
6961
) => {
7062
elizaLogger.log("Starting CREATE_TOKEN handler...");
7163

@@ -91,9 +83,11 @@ export default {
9183
});
9284

9385
const payload = content.object as CreateTokenContent;
86+
const isCreateTokenContent =
87+
payload.tokenName && payload.tokenName && payload.tokenName;
9488

9589
// Validate transfer content
96-
if (!isCreateTokenContent(runtime, payload)) {
90+
if (!isCreateTokenContent) {
9791
console.error("Invalid content for CREATE_TOKEN action.");
9892
if (callback) {
9993
callback({
@@ -110,12 +104,17 @@ export default {
110104

111105
const walletProvider = new WalletProvider(privateKey, network);
112106

113-
await walletProvider.createESDT({
107+
const txHash = await walletProvider.createESDT({
114108
tokenName: payload.tokenName,
115109
amount: payload.amount,
116110
decimals: Number(payload.decimals) || 18,
117111
tokenTicker: payload.tokenTicker,
118112
});
113+
114+
const txURL = walletProvider.getTransactionURL(txHash);
115+
callback?.({
116+
text: `Transaction sent successfully! You can view it here: ${txURL}.`,
117+
});
119118
return true;
120119
} catch (error) {
121120
console.error("Error during creating token:", error);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import {
2+
elizaLogger,
3+
type ActionExample,
4+
type Content,
5+
type HandlerCallback,
6+
type IAgentRuntime,
7+
type Memory,
8+
ModelClass,
9+
type State,
10+
composeContext,
11+
generateObject,
12+
type Action,
13+
} from "@elizaos/core";
14+
import { WalletProvider } from "../providers/wallet";
15+
import { GraphqlProvider } from "../providers/graphql";
16+
import { validateMultiversxConfig } from "../enviroment";
17+
import { swapSchema } from "../utils/schemas";
18+
import { MVX_NETWORK_CONFIG } from "../constants";
19+
import { swapQuery } from "../graphql/swapQuery";
20+
import { NativeAuthProvider } from "../providers/nativeAuth";
21+
import {
22+
FungibleTokenOfAccountOnNetwork,
23+
Transaction,
24+
TransactionPayload,
25+
} from "@multiversx/sdk-core/out";
26+
import { denominateAmount, getRawAmount } from "../utils/amount";
27+
import { getToken } from "../utils/getToken";
28+
import { filteredTokensQuery } from "../graphql/tokensQuery";
29+
30+
type SwapResultType = {
31+
swap: {
32+
noAuthTransactions: {
33+
value: string;
34+
receiver: string;
35+
gasPrice: bigint;
36+
gasLimit: bigint;
37+
data: TransactionPayload;
38+
chainID: string;
39+
version: number;
40+
sender: string;
41+
nonce: number;
42+
}[];
43+
};
44+
};
45+
46+
export interface ISwapContent extends Content {
47+
tokenIn: string;
48+
amountIn: string;
49+
tokenOut: string;
50+
}
51+
52+
const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
53+
54+
Example response:
55+
\`\`\`json
56+
{
57+
"tokenIn": "EGLD",
58+
"amountIn": "1",
59+
"tokenOut": "MEX"
60+
}
61+
\`\`\`
62+
63+
{{recentMessages}}
64+
65+
Given the recent messages, extract the following information about the requested token transfer:
66+
- Source token
67+
- Amount to transfer
68+
- Destination token
69+
70+
Respond with a JSON markdown block containing only the extracted values.`;
71+
72+
export default {
73+
name: "SWAP",
74+
similes: ["SWAP_TOKEN", "SWAP_TOKENS"],
75+
validate: async (runtime: IAgentRuntime, message: Memory) => {
76+
console.log("Validating config for user:", message.userId);
77+
await validateMultiversxConfig(runtime);
78+
return true;
79+
},
80+
description: "Swap tokens",
81+
handler: async (
82+
runtime: IAgentRuntime,
83+
message: Memory,
84+
state: State,
85+
_options: { [key: string]: unknown },
86+
callback?: HandlerCallback,
87+
) => {
88+
elizaLogger.log("Starting SWAP handler...");
89+
90+
// Initialize or update state
91+
if (!state) {
92+
state = (await runtime.composeState(message)) as State;
93+
} else {
94+
state = await runtime.updateRecentMessageState(state);
95+
}
96+
97+
// Compose transfer context
98+
const swapContext = composeContext({
99+
state,
100+
template: swapTemplate,
101+
});
102+
103+
// Generate transfer content
104+
const content = await generateObject({
105+
runtime,
106+
context: swapContext,
107+
modelClass: ModelClass.SMALL,
108+
schema: swapSchema,
109+
});
110+
111+
const swapContent = content.object as ISwapContent;
112+
113+
const isSwapContent =
114+
typeof swapContent.tokenIn === "string" &&
115+
typeof swapContent.tokenOut === "string" &&
116+
typeof swapContent.amountIn === "string";
117+
118+
// Validate transfer content
119+
if (!isSwapContent) {
120+
console.error("Invalid content for SWAP action.");
121+
122+
callback?.({
123+
text: "Unable to process swap request. Invalid content provided.",
124+
content: { error: "Invalid swap content" },
125+
});
126+
127+
return false;
128+
}
129+
130+
try {
131+
const privateKey = runtime.getSetting("MVX_PRIVATE_KEY");
132+
const network = runtime.getSetting("MVX_NETWORK");
133+
const networkConfig = MVX_NETWORK_CONFIG[network];
134+
const walletProvider = new WalletProvider(privateKey, network);
135+
const isEGLD = swapContent.tokenIn.toLowerCase() === "egld";
136+
137+
const hasEgldBalance = await walletProvider.hasEgldBalance(
138+
isEGLD ? swapContent.amountIn : undefined,
139+
);
140+
141+
if (!hasEgldBalance) {
142+
throw new Error("Insufficient balance.");
143+
}
144+
145+
const nativeAuthProvider = new NativeAuthProvider({
146+
apiUrl: networkConfig.apiURL,
147+
});
148+
149+
await nativeAuthProvider.initializeClient();
150+
const address = walletProvider.getAddress().toBech32();
151+
152+
const accessToken =
153+
await nativeAuthProvider.getAccessToken(walletProvider);
154+
155+
const graphqlProvider = new GraphqlProvider(
156+
networkConfig.graphURL,
157+
{ Authorization: `Bearer ${accessToken}` },
158+
);
159+
160+
let tokenData: FungibleTokenOfAccountOnNetwork = null;
161+
let tokenIn = swapContent.tokenIn;
162+
let tokenOut = swapContent.tokenOut;
163+
const [tickerOut, nonceOut] = swapContent.tokenOut.split("-");
164+
165+
if (!isEGLD) {
166+
const [tickerIn, nonceIn] = swapContent.tokenIn.split("-");
167+
168+
if (!nonceIn) {
169+
const token = await getToken({
170+
provider: graphqlProvider,
171+
ticker: tickerIn,
172+
});
173+
tokenIn = token.identifier;
174+
}
175+
176+
if (!nonceOut && tickerOut.toLowerCase() !== "egld") {
177+
const token = await getToken({
178+
provider: graphqlProvider,
179+
ticker: tickerOut,
180+
});
181+
tokenOut = token.identifier;
182+
}
183+
184+
tokenData = await walletProvider.getTokenData(tokenIn);
185+
186+
const rawBalance = getRawAmount({
187+
amount: tokenData.balance.toString(),
188+
decimals: tokenData.rawResponse.decimals,
189+
});
190+
const rawBalanceNum = Number(rawBalance);
191+
192+
if (rawBalanceNum < Number(swapContent.amountIn)) {
193+
throw new Error("Insufficient balance");
194+
}
195+
}
196+
197+
if (!nonceOut && tickerOut.toLowerCase() !== "egld") {
198+
const token = await getToken({
199+
provider: graphqlProvider,
200+
ticker: tickerOut,
201+
});
202+
tokenOut = token.identifier;
203+
}
204+
205+
const value = denominateAmount({
206+
amount: swapContent.amountIn,
207+
decimals: isEGLD ? 18 : tokenData?.rawResponse?.decimals,
208+
});
209+
210+
const variables = {
211+
amountIn: value,
212+
tokenInID: tokenIn,
213+
tokenOutID: tokenOut,
214+
tolerance: 0.01,
215+
sender: address,
216+
};
217+
218+
const { swap } = await graphqlProvider.query<SwapResultType>(
219+
swapQuery,
220+
variables,
221+
);
222+
223+
if (!swap.noAuthTransactions) {
224+
throw new Error("No route found");
225+
}
226+
227+
const txURLs = await Promise.all(
228+
swap.noAuthTransactions.map(async (transaction) => {
229+
const txToBroadcast = { ...transaction };
230+
txToBroadcast.sender = address;
231+
txToBroadcast.data = TransactionPayload.fromEncoded(
232+
transaction.data as unknown as string,
233+
);
234+
235+
const account = await walletProvider.getAccount(
236+
walletProvider.getAddress(),
237+
);
238+
txToBroadcast.nonce = account.nonce;
239+
240+
const tx = new Transaction(txToBroadcast);
241+
const signature = await walletProvider.signTransaction(tx);
242+
tx.applySignature(signature);
243+
244+
const txHash = await walletProvider.sendTransaction(tx);
245+
return walletProvider.getTransactionURL(txHash); // Return the transaction URL
246+
}),
247+
);
248+
249+
const transactionURLs = txURLs.join(",");
250+
callback?.({
251+
text: `Transaction(s) sent successfully! You can view it here: ${transactionURLs}.`,
252+
});
253+
return true;
254+
} catch (error) {
255+
console.error("Error during token swap:", error);
256+
callback?.({
257+
text: "Could not execute the swap.",
258+
content: { error: error.message },
259+
});
260+
261+
return "";
262+
}
263+
},
264+
265+
examples: [
266+
[
267+
{
268+
user: "{{user1}}",
269+
content: {
270+
text: "Swap 1 EGLD for USDC",
271+
},
272+
},
273+
{
274+
user: "{{agent}}",
275+
content: {
276+
text: "Swapping 1 EGLD for USDC...",
277+
},
278+
},
279+
],
280+
] as ActionExample[][],
281+
} as Action;

0 commit comments

Comments
 (0)