Skip to content

Commit 96eaba2

Browse files
feat: Cosmos Plugin - IBC swap action (elizaOS#2554)
* feat: ibc transfer action * refactor: remove redundant services and use skipClient * update: eliza package import * feat: add bridge data fetcher and update tests * update: add IBC transfer action to README * refactor: make bridge data fetcher reusable across the project * fix: update similes for ibc transfer action * ELIZAAI-18 IBC swap action initialisation added * ELIZAAI-18 Added error handling, improved prompt data extracting, fixed denom fetching * ELIZAAI-18 Unit tests added, data extracting template adjusted and character directives updated * ELIZAAI-18 Switched to chain-registry function for fetching token exponent * fix: update toAddress in ibc transfer * ELIZAAI-18 Updated Readme * ELIZAAI-18 removed unused character settings, refactored names in ibc-swap * ELIZAAI-18 removed unused character settings, refactored names in ibc-swap * Fix unit tests, adjust custom character * Code review fixes * Fix pnpm-lock --------- Co-authored-by: KacperKoza34 <kacper.koza@blockydevs.com> Co-authored-by: KacperKoza343 <165884432+KacperKoza343@users.noreply.github.com>
1 parent 4240c65 commit 96eaba2

14 files changed

+929
-19
lines changed

characters/cosmosHelper.character.json

+18-10
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"settings": {
66
"voice": {
77
"model": "en_US-male-medium"
8-
},
9-
"chains": {
10-
"cosmos": ["axelar", "carbon", "mantrachaintestnet2"]
118
}
129
},
1310
"plugins": [],
@@ -26,14 +23,17 @@
2623
"Knows how Cosmos blockchain works",
2724
"Knows what actions should be called for token transfer, swapping or bridging",
2825
"Knows that users might want to do specific actions multiple times and should help them by doing it again.",
29-
"Should always ask for confirmation before calling an COSMOS_TRANSFER, COSMOS_BRIDGE, COSMOS_SWAP actions.",
30-
"Should call actions COSMOS_TRANSFER, COSMOS_BRIDGE, COSMOS_SWAP only after previous confirmation."
26+
"Should always ask for confirmation before calling an COSMOS_TRANSFER, COSMOS_BRIDGE, COSMOS_IBC_SWAP actions.",
27+
"Should ask for confirmation ONCE and perform action after getting it. If user wants to change sth in data for transfer, should do it and ask again for confirmation of new data.",
28+
"Should call actions COSMOS_TRANSFER, COSMOS_BRIDGE, COSMOS_IBC_SWAP only after previous confirmation."
3129
],
3230
"messageExamples": [
3331
[
3432
{
3533
"user": "{{user1}}",
36-
"content": { "text": "Show my balances of my wallet on {{mantrachaintestnet2}}" }
34+
"content": {
35+
"text": "Show my balances of my wallet on {{mantrachaintestnet2}}"
36+
}
3737
},
3838
{
3939
"user": "CosmosHelper",
@@ -45,7 +45,9 @@
4545
[
4646
{
4747
"user": "{{user1}}",
48-
"content": { "text": "How does IBC work?" }
48+
"content": {
49+
"text": "How does IBC work?"
50+
}
4951
},
5052
{
5153
"user": "CosmosHelper",
@@ -57,7 +59,9 @@
5759
[
5860
{
5961
"user": "{{user1}}",
60-
"content": { "text": "What is CosmWasm?" }
62+
"content": {
63+
"text": "What is CosmWasm?"
64+
}
6165
},
6266
{
6367
"user": "CosmosHelper",
@@ -69,7 +73,9 @@
6973
[
7074
{
7175
"user": "{{user1}}",
72-
"content": { "text": "Can you help me transfer tokens?" }
76+
"content": {
77+
"text": "Can you help me transfer tokens?"
78+
}
7379
},
7480
{
7581
"user": "CosmosHelper",
@@ -81,7 +87,9 @@
8187
[
8288
{
8389
"user": "{{user1}}",
84-
"content": { "text": "Make transfer 0.0001 OM to mantra13248w8dtnn07sxc3gq4l3ts4rvfyat6fks0ecj on mantrachaintestnet2" }
90+
"content": {
91+
"text": "Make transfer 0.0001 OM to mantra13248w8dtnn07sxc3gq4l3ts4rvfyat6fks0ecj on mantrachaintestnet2"
92+
}
8593
},
8694
{
8795
"user": "CosmosHelper",

packages/plugin-cosmos/README.md

+72
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,78 @@ Yes
134134

135135
---
136136

137+
### Token IBC Swap
138+
This action allows swapping tokens between chains. The implementation of swapping is based on the Skip API and uses the @skip-go/client library.
139+
To place transactions on chains, they must first be added to the env file. Specifically:
140+
```env
141+
COSMOS_AVAILABLE_CHAINS=osmosis,neutron,axelar,cosmoshub,terra2,pryzm
142+
```
143+
Keep in mind that most swaps require intermediate chains. These chains must also be included in the env file.
144+
145+
You can check which chains are supported by the Skip API and this plugin here: [Skip API Documentation](https://docs.skip.build/).
146+
147+
The list below contains all supported chains extracted from the Skip API:
148+
```env
149+
COSMOS_AVAILABLE_CHAINS=terra2,quicksilver,coreum,regen,mars,passage,dhealth,lumnetwork,provenance,chihuahua,pryzm,fetchhub,comdex,kyve,xpla,umee,celestia,osmosis,empowerchain,migaloo,dymension,kujira,self,humans,gitopia,agoric,doravota,int3face,quasar,gravitybridge,kava,sifchain,seda,shentu,decentr,cronos,carbon,stride,haqq,jackal,omniflixhub,noble,sentinel,nolus,odin,assetmantle,dydx,beezee,axelar,nois,mantrachain,elys,crescent,synternet,xion,source,akash,sei,canto,teritori,stargaze,titan,aura,evmos,archway,gateway,cheqd,milkyway,nibiru,cosmoshub,ununifi,nyx,bitsong,cryptoorgchain,neutron,penumbra,terra,shido,sommelier,saga,secretnetwork,chain4energy,juno,andromeda,planq,lava,oraichain,injective,persistence,composable
150+
```
151+
152+
#### Example prompts:
153+
154+
##### Default flow
155+
An example of the default flow of a swap request:
156+
157+
1. User input:
158+
```
159+
Swap 10 OSMO on osmosis to ATOM on cosmoshub
160+
```
161+
2. Agent asks for confirmation:
162+
```
163+
About to swap 10 OSMO on osmosis to ATAOM on cosmoshub. I would like to confirm the transaction details. Please confirm that you would like to proceed with the swap.
164+
```
165+
3. User confiramation:
166+
```
167+
confirming / yes
168+
```
169+
4. Agent calls action and responds after completing:
170+
```
171+
Successfuly swapped 10 OSMO tokens to ATOM on chain cosmoshub.
172+
Transaction Hash: E84F36D6EEFAA6D7B70827F34EDAB83258BB86EFE33AEA1F4559D00B30CD3B50
173+
```
174+
175+
##### Special case handling
176+
An example of a flow when there is more than one token with the same symbol on the destination chain (e.g., multiple USDC tokens):
177+
1. User input:
178+
```
179+
Swap 10 OSMO on osmosis to ATOM on cosmoshub
180+
```
181+
2. Agent asks for confirmation:
182+
```
183+
About to swap 10 OSMO on osmosis to ATAOM on cosmoshub. I would like to confirm the transaction details. Please confirm that you would like to proceed with the swap.
184+
```
185+
3. User confirmation:
186+
```
187+
confirming / yes
188+
```
189+
4. Model response:
190+
```
191+
I will now initiate the swap of 10 OSMO on the Osmosis chain to USDC on the Axelar chain. Please wait for the transaction to complete. If you need to swap more assets, feel free to let me know.
192+
193+
Error occured. Swap was not performed. Please provide denom for coin: USDC, on Chain Name: axelar. It is necessary as the symbol USDC is not unique among coins on chain axelar.
194+
Select one from found assets:
195+
Symbol: USDC Desc: Circle's stablecoin on Axelar Denom: uusdc,
196+
Symbol: USDC Desc: Circle's stablecoin from Polygon on Axelar Denom: polygon-uusdc,
197+
Symbol: USDC Desc: Circle's stablecoin from Avalanche on Axelar Denom: avalanche-uusdc
198+
```
199+
5. User response:
200+
```
201+
Swap 10 OSMO on osmosis to USDC with denom uusdc on axelar
202+
```
203+
6. Action call and agent response:
204+
```
205+
Successfuly swapped 10 OSMO tokens to USDC uusdc on chain axelar.
206+
Transaction Hash: E84F36D6EEFAA6D7B70827F34EDAB83258BB86EFE33AEA1F4559D00B30CD3B50
207+
```
208+
137209
## Contribution
138210

139211
The plugin includes comprehensive tests. Before submitting any pull requests, ensure all tests pass.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
composeContext,
3+
generateObjectDeprecated,
4+
HandlerCallback,
5+
IAgentRuntime,
6+
Memory,
7+
ModelClass,
8+
State,
9+
} from "@elizaos/core";
10+
11+
import { initWalletChainsData } from "../../providers/wallet/utils";
12+
import { cosmosIBCSwapTemplate } from "../../templates";
13+
import type {
14+
ICosmosPluginOptions,
15+
ICosmosWalletChains,
16+
} from "../../shared/interfaces";
17+
import { IBCSwapActionParams } from "./types.ts";
18+
import { IBCSwapAction } from "./services/ibc-swap-action-service.ts";
19+
import { prepareAmbiguityErrorMessage } from "./services/ibc-swap-utils.ts";
20+
21+
export const createIBCSwapAction = (pluginOptions: ICosmosPluginOptions) => ({
22+
name: "COSMOS_IBC_SWAP",
23+
description: "Swaps tokens on cosmos chains",
24+
handler: async (
25+
_runtime: IAgentRuntime,
26+
_message: Memory,
27+
state: State,
28+
_options: { [key: string]: unknown },
29+
_callback?: HandlerCallback
30+
) => {
31+
const cosmosIBCSwapContext = composeContext({
32+
state: state,
33+
template: cosmosIBCSwapTemplate,
34+
templatingEngine: "handlebars",
35+
});
36+
37+
const cosmosIBCSwapContent = await generateObjectDeprecated({
38+
runtime: _runtime,
39+
context: cosmosIBCSwapContext,
40+
modelClass: ModelClass.SMALL,
41+
});
42+
43+
const paramOptions: IBCSwapActionParams = {
44+
fromChainName: cosmosIBCSwapContent.fromChainName,
45+
fromTokenSymbol: cosmosIBCSwapContent.fromTokenSymbol,
46+
fromTokenAmount: cosmosIBCSwapContent.fromTokenAmount,
47+
toTokenSymbol: cosmosIBCSwapContent.toTokenSymbol,
48+
toChainName: cosmosIBCSwapContent.toChainName,
49+
toTokenDenom: cosmosIBCSwapContent?.toTokenDenom || undefined,
50+
fromTokenDenom: cosmosIBCSwapContent?.fromTokenDenom || undefined,
51+
};
52+
53+
console.log(
54+
"Parameters extracted from user prompt: ",
55+
JSON.stringify(paramOptions, null, 2)
56+
);
57+
58+
try {
59+
const walletProvider: ICosmosWalletChains =
60+
await initWalletChainsData(_runtime);
61+
62+
const action = new IBCSwapAction(walletProvider);
63+
64+
const customAssets = (pluginOptions?.customChainData ?? []).map(
65+
(chainData) => chainData.assets
66+
);
67+
68+
if (_callback) {
69+
70+
const swapResp = await action.execute(
71+
paramOptions,
72+
customAssets,
73+
_callback
74+
);
75+
76+
const text =
77+
swapResp.status === "STATE_COMPLETED_SUCCESS"
78+
? `Successfully swapped ${swapResp.fromTokenAmount} ${swapResp.fromTokenSymbol} tokens to ${swapResp.toTokenSymbol} on chain ${swapResp.toChainName}.\nTransaction Hash: ${swapResp.txHash}`
79+
: `Error occured swapping ${swapResp.fromTokenAmount} ${swapResp.fromTokenSymbol} tokens to ${swapResp.toTokenSymbol} on chain ${swapResp.toChainName}.\nTransaction Hash: ${swapResp.txHash}, try again`;
80+
await _callback({
81+
text: text,
82+
content: {
83+
success:
84+
swapResp.status === "STATE_COMPLETED_SUCCESS",
85+
hash: swapResp.txHash,
86+
fromTokenAmount: paramOptions.fromTokenAmount,
87+
fromToken: paramOptions.fromTokenSymbol,
88+
toToken: paramOptions.toTokenSymbol,
89+
fromChain: paramOptions.fromChainName,
90+
toChain: paramOptions.toChainName,
91+
},
92+
});
93+
}
94+
return true;
95+
} catch (error) {
96+
console.error("Error during ibc token swap:", error);
97+
98+
const regex =
99+
/Ambiguity Error.*value:([^\s.]+)\s+chainName:([^\s.]+)/;
100+
const match = error.message.match(regex);
101+
102+
if (match) {
103+
const value = match[1];
104+
const chainName = match[2];
105+
106+
if (_callback) {
107+
await _callback({
108+
text: prepareAmbiguityErrorMessage(value, chainName),
109+
content: { error: error.message },
110+
});
111+
}
112+
} else {
113+
console.error("Unhandled error:", error);
114+
115+
if (_callback) {
116+
await _callback({
117+
text: `Error ibc swapping tokens: ${error.message}`,
118+
content: { error: error.message },
119+
});
120+
}
121+
}
122+
return false;
123+
}
124+
},
125+
template: cosmosIBCSwapTemplate,
126+
validate: async (runtime: IAgentRuntime) => {
127+
const mnemonic = runtime.getSetting("COSMOS_RECOVERY_PHRASE");
128+
const availableChains = runtime.getSetting("COSMOS_AVAILABLE_CHAINS");
129+
const availableChainsArray = availableChains?.split(",");
130+
131+
return !(mnemonic && availableChains && availableChainsArray.length);
132+
},
133+
examples: [
134+
[
135+
{
136+
user: "{{user1}}",
137+
content: {
138+
text: "Swap {{0.0001 ATOM}} from {{cosmoshub}} to {{OM}} on {{mantrachain1}}",
139+
action: "COSMOS_IBC_SWAP",
140+
},
141+
},
142+
{
143+
user: "{{user2}}",
144+
content: {
145+
text: "Do you confirm the swap?",
146+
action: "COSMOS_IBC_SWAP",
147+
},
148+
},
149+
{
150+
user: "{{user1}}",
151+
content: {
152+
text: "Yes",
153+
action: "COSMOS_IBC_SWAP",
154+
},
155+
},
156+
{
157+
user: "{{user2}}",
158+
content: {
159+
text: "Starting swap transaction. Keep in mind that it might take couple of minutes",
160+
action: "COSMOS_IBC_SWAP",
161+
},
162+
},
163+
],
164+
[
165+
{
166+
user: "{{user1}}",
167+
content: {
168+
text: "Swap {{0.0001 OM}} from {{mantrachain}} to {{OSMO}} on {{osmosis}}",
169+
action: "COSMOS_IBC_SWAP",
170+
},
171+
},
172+
{
173+
user: "{{user2}}",
174+
content: {
175+
text: "Do you confirm the swap?",
176+
action: "COSMOS_IBC_SWAP",
177+
},
178+
},
179+
{
180+
user: "{{user1}}",
181+
content: {
182+
text: "Yes",
183+
action: "COSMOS_IBC_SWAP",
184+
},
185+
},
186+
{
187+
user: "{{user2}}",
188+
content: {
189+
text: "Starting swap transaction. Keep in mind that it might take couple of minutes",
190+
action: "COSMOS_IBC_SWAP",
191+
},
192+
},
193+
],
194+
],
195+
similes: ["COSMOS_SWAP", "COSMOS_SWAP_IBC"],
196+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {z} from "zod";
2+
3+
export const IBCSwapParamsSchema = z.object({
4+
fromChainName: z.string().min(1),
5+
fromTokenSymbol: z.string().regex(/^[A-Z0-9]+$/),
6+
fromTokenAmount: z.string().regex(/^\d+$/),
7+
toTokenSymbol: z.string().regex(/^[A-Z0-9]+$/),
8+
toChainName: z.string().min(1),
9+
toTokenDenom: z.string().regex(/^ibc\/[A-F0-9]{64}$/),
10+
fromTokenDenom: z.string().regex(/^ibc\/[A-F0-9]{64}$/),
11+
});

0 commit comments

Comments
 (0)