Skip to content

Commit cd0388d

Browse files
feat: ibc transfer on cosmos blockchains (#2358)
* 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 * fix: update toAddress in ibc transfer * refactor: add changes according to review * fix: ibc transfer action
1 parent fac4e81 commit cd0388d

21 files changed

+1210
-4
lines changed

packages/plugin-cosmos/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,36 @@ Yes
102102

103103
4. Action executed.
104104

105+
### Token IBC Transfer
106+
107+
This plugin supports a token transfer action, which allows users to transfer tokens between addresses on Cosmos-compatible blockchains between different chains.
108+
109+
#### Example Prompts
110+
111+
Below are examples of how the ibc transfer action can be initiated and confirmed:
112+
113+
**Example**
114+
115+
1. User input:
116+
117+
```
118+
Make an IBC transfer 0.0001 OSMO to neutron1nk3uuw6zt5t5aqw5fvujkd54sa4uws9xg2nk82 from osmosistestnet to neutrontestnet
119+
```
120+
121+
2. Plugin response:
122+
123+
```
124+
Before making the IBC transfer, I would like to confirm the details. You would like to transfer 0.0001 OSMO from osmosistestnet to neutrontestnet, specifically to the address neutron1nk3uuw6zt5t5aqw5fvujkd54sa4uws9xg2nk82, is that correct?
125+
```
126+
127+
3. User confirmation:
128+
129+
```
130+
Yes
131+
```
132+
133+
4. Action executed.
134+
105135
---
106136

107137
## Contribution

packages/plugin-cosmos/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"@cosmjs/cosmwasm-stargate": "^0.32.4",
1111
"@cosmjs/proto-signing": "^0.32.4",
1212
"@cosmjs/stargate": "^0.32.4",
13+
"@skip-go/client": "^0.16.3",
14+
"axios": "^1.7.9",
1315
"bignumber.js": "9.1.2",
1416
"chain-registry": "^1.69.68",
1517
"tsup": "8.3.5",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import {
2+
composeContext,
3+
generateObjectDeprecated,
4+
HandlerCallback,
5+
IAgentRuntime,
6+
Memory,
7+
ModelClass,
8+
State,
9+
} from "@elizaos/core";
10+
import { initWalletChainsData } from "../../providers/wallet/utils";
11+
import {
12+
cosmosIBCTransferTemplate,
13+
cosmosTransferTemplate,
14+
} from "../../templates";
15+
import type {
16+
ICosmosPluginOptions,
17+
ICosmosWalletChains,
18+
} from "../../shared/interfaces";
19+
import { IBCTransferActionParams } from "./types";
20+
import { IBCTransferAction } from "./services/ibc-transfer-action-service";
21+
import { bridgeDenomProvider } from "./services/bridge-denom-provider";
22+
23+
export const createIBCTransferAction = (
24+
pluginOptions: ICosmosPluginOptions
25+
) => ({
26+
name: "COSMOS_IBC_TRANSFER",
27+
description: "Transfer tokens between addresses on cosmos chains",
28+
handler: async (
29+
_runtime: IAgentRuntime,
30+
_message: Memory,
31+
state: State,
32+
_options: { [key: string]: unknown },
33+
_callback?: HandlerCallback
34+
) => {
35+
const cosmosIBCTransferContext = composeContext({
36+
state: state,
37+
template: cosmosIBCTransferTemplate,
38+
templatingEngine: "handlebars",
39+
});
40+
41+
const cosmosIBCTransferContent = await generateObjectDeprecated({
42+
runtime: _runtime,
43+
context: cosmosIBCTransferContext,
44+
modelClass: ModelClass.SMALL,
45+
});
46+
47+
const paramOptions: IBCTransferActionParams = {
48+
chainName: cosmosIBCTransferContent.chainName,
49+
symbol: cosmosIBCTransferContent.symbol,
50+
amount: cosmosIBCTransferContent.amount,
51+
toAddress: cosmosIBCTransferContent.toAddress,
52+
targetChainName: cosmosIBCTransferContent.targetChainName,
53+
};
54+
55+
try {
56+
const walletProvider: ICosmosWalletChains =
57+
await initWalletChainsData(_runtime);
58+
59+
const action = new IBCTransferAction(walletProvider);
60+
61+
const customAssets = (pluginOptions?.customChainData ?? []).map(
62+
(chainData) => chainData.assets
63+
);
64+
65+
const transferResp = await action.execute(
66+
paramOptions,
67+
bridgeDenomProvider,
68+
customAssets
69+
);
70+
71+
if (_callback) {
72+
await _callback({
73+
text: `Successfully transferred ${paramOptions.amount} tokens from ${paramOptions.chainName} to ${paramOptions.toAddress} on ${paramOptions.targetChainName}\nTransaction Hash: ${transferResp.txHash}`,
74+
content: {
75+
success: true,
76+
hash: transferResp.txHash,
77+
amount: paramOptions.amount,
78+
recipient: transferResp.to,
79+
fromChain: paramOptions.chainName,
80+
toChain: paramOptions.targetChainName,
81+
},
82+
});
83+
84+
const newMemory: Memory = {
85+
userId: _message.agentId,
86+
agentId: _message.agentId,
87+
roomId: _message.roomId,
88+
content: {
89+
text: `Transaction ${paramOptions.amount} ${paramOptions.symbol} to address ${paramOptions.toAddress} from chain ${paramOptions.chainName} to ${paramOptions.targetChainName} was successfully transferred. Tx hash: ${transferResp.txHash}`,
90+
},
91+
};
92+
93+
await _runtime.messageManager.createMemory(newMemory);
94+
}
95+
return true;
96+
} catch (error) {
97+
console.error("Error during ibc token transfer:", error);
98+
99+
if (_callback) {
100+
await _callback({
101+
text: `Error ibc transferring tokens: ${error.message}`,
102+
content: { error: error.message },
103+
});
104+
}
105+
106+
const newMemory: Memory = {
107+
userId: _message.agentId,
108+
agentId: _message.agentId,
109+
roomId: _message.roomId,
110+
content: {
111+
text: `Transaction ${paramOptions.amount} ${paramOptions.symbol} to address ${paramOptions.toAddress} on chain ${paramOptions.chainName} to ${paramOptions.targetChainName} was unsuccessful.`,
112+
},
113+
};
114+
115+
await _runtime.messageManager.createMemory(newMemory);
116+
117+
return false;
118+
}
119+
},
120+
template: cosmosTransferTemplate,
121+
validate: async (runtime: IAgentRuntime) => {
122+
const mnemonic = runtime.getSetting("COSMOS_RECOVERY_PHRASE");
123+
const availableChains = runtime.getSetting("COSMOS_AVAILABLE_CHAINS");
124+
const availableChainsArray = availableChains?.split(",");
125+
126+
return !!(mnemonic && availableChains && availableChainsArray.length);
127+
},
128+
examples: [
129+
[
130+
{
131+
user: "{{user1}}",
132+
content: {
133+
text: "Make an IBC transfer {{0.0001 ATOM}} to {{osmosis1pcnw46km8m5amvf7jlk2ks5std75k73aralhcf}} from {{cosmoshub}} to {{osmosis}}",
134+
action: "COSMOS_IBC_TRANSFER",
135+
},
136+
},
137+
{
138+
user: "{{user2}}",
139+
content: {
140+
text: "Do you confirm the IBC transfer action?",
141+
action: "COSMOS_IBC_TRANSFER",
142+
},
143+
},
144+
{
145+
user: "{{user1}}",
146+
content: {
147+
text: "Yes",
148+
action: "COSMOS_IBC_TRANSFER",
149+
},
150+
},
151+
{
152+
user: "{{user2}}",
153+
content: {
154+
text: "",
155+
action: "COSMOS_IBC_TRANSFER",
156+
},
157+
},
158+
],
159+
[
160+
{
161+
user: "{{user1}}",
162+
content: {
163+
text: "Send {{50 OSMO}} to {{juno13248w8dtnn07sxc3gq4l3ts4rvfyat6f4qkdd6}} from {{osmosis}} to {{juno}}",
164+
action: "COSMOS_IBC_TRANSFER",
165+
},
166+
},
167+
{
168+
user: "{{user2}}",
169+
content: {
170+
text: "Do you confirm the IBC transfer action?",
171+
action: "COSMOS_IBC_TRANSFER",
172+
},
173+
},
174+
{
175+
user: "{{user1}}",
176+
content: {
177+
text: "Yes",
178+
action: "COSMOS_IBC_TRANSFER",
179+
},
180+
},
181+
{
182+
user: "{{user2}}",
183+
content: {
184+
text: "",
185+
action: "COSMOS_IBC_TRANSFER",
186+
},
187+
},
188+
],
189+
[
190+
{
191+
user: "{{user1}}",
192+
content: {
193+
text: "Transfer {{0.005 JUNO}} from {{juno}} to {{cosmos1n0xv7z2pkl4eppnm7g2rqhe2q8q6v69h7w93fc}} on {{cosmoshub}}",
194+
action: "COSMOS_IBC_TRANSFER",
195+
},
196+
},
197+
{
198+
user: "{{user2}}",
199+
content: {
200+
text: "Do you confirm the IBC transfer action?",
201+
action: "COSMOS_IBC_TRANSFER",
202+
},
203+
},
204+
{
205+
user: "{{user1}}",
206+
content: {
207+
text: "Yes",
208+
action: "COSMOS_IBC_TRANSFER",
209+
},
210+
},
211+
{
212+
user: "{{user2}}",
213+
content: {
214+
text: "",
215+
action: "COSMOS_IBC_TRANSFER",
216+
},
217+
},
218+
],
219+
],
220+
similes: [
221+
"COSMOS_BRIDGE_TOKEN",
222+
"COSMOS_IBC_SEND_TOKEN",
223+
"COSMOS_TOKEN_IBC_TRANSFER",
224+
"COSMOS_MOVE_IBC_TOKENS",
225+
],
226+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from "zod";
2+
3+
export const IBCTransferParamsSchema = z.object({
4+
chainName: z.string(),
5+
symbol: z.string(),
6+
amount: z.string().regex(/^\d+$/, "Amount must be a numeric string"),
7+
toAddress: z.string().regex(/^[a-z0-9]+$/, "Invalid bech32 address format"),
8+
targetChainName: z.string(),
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { IDenomProvider } from "../../../shared/interfaces";
2+
import { SkipApiAssetsFromSourceFetcher } from "../../../shared/services/skip-api/assets-from-source-fetcher/skip-api-assets-from-source-fetcher";
3+
4+
export const bridgeDenomProvider: IDenomProvider = async (
5+
sourceAssetDenom: string,
6+
sourceAssetChainId: string,
7+
destChainId: string
8+
) => {
9+
const skipApiAssetsFromSourceFetcher =
10+
SkipApiAssetsFromSourceFetcher.getInstance();
11+
const bridgeData = await skipApiAssetsFromSourceFetcher.fetch(
12+
sourceAssetDenom,
13+
sourceAssetChainId
14+
);
15+
16+
const destAssets = bridgeData.dest_assets[destChainId];
17+
18+
if (!destAssets?.assets) {
19+
throw new Error(`No assets found for chain ${destChainId}`);
20+
}
21+
22+
const ibcAssetData = destAssets.assets?.find(
23+
({ origin_denom }) => origin_denom === sourceAssetDenom
24+
);
25+
26+
if (!ibcAssetData) {
27+
throw new Error(`No matching asset found for denom ${sourceAssetDenom}`);
28+
}
29+
30+
if (!ibcAssetData.denom) {
31+
throw new Error("No IBC asset data");
32+
}
33+
34+
return {
35+
denom: ibcAssetData.denom,
36+
};
37+
};

0 commit comments

Comments
 (0)