Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ibc transfer action #14

Merged
1 change: 1 addition & 0 deletions packages/plugin-cosmos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"axios": "^1.7.9",
"bignumber.js": "9.1.2",
"chain-registry": "^1.69.68",
"tsup": "8.3.5",
Expand Down
226 changes: 226 additions & 0 deletions packages/plugin-cosmos/src/actions/ibc-transfer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import {
composeContext,
generateObjectDeprecated,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
} from "@ai16z/eliza";
import { initWalletChainsData } from "../../providers/wallet/utils";
import {
cosmosIBCTransferTemplate,
cosmosTransferTemplate,
} from "../../templates";
import type {
ICosmosPluginOptions,
ICosmosWalletChains,
} from "../../shared/interfaces";
import { IBCTransferActionParams } from "./types";
import { CosmosIBCTransferAction } from "./services/ibc-transfer-action-service";
import { bridgeDataProvider } from "./services/bridge-data-provider";

export const createIBCTransferAction = (
pluginOptions: ICosmosPluginOptions
) => ({
name: "COSMOS_IBC_TRANSFER",
description: "Transfer tokens between addresses on cosmos chains",
handler: async (
_runtime: IAgentRuntime,
_message: Memory,
state: State,
_options: { [key: string]: unknown },
_callback?: HandlerCallback
) => {
const cosmosIBCTransferContext = composeContext({
state: state,
template: cosmosIBCTransferTemplate,
templatingEngine: "handlebars",
});

const cosmosIBCTransferContent = await generateObjectDeprecated({
runtime: _runtime,
context: cosmosIBCTransferContext,
modelClass: ModelClass.SMALL,
});

const paramOptions: IBCTransferActionParams = {
chainName: cosmosIBCTransferContent.chainName,
symbol: cosmosIBCTransferContent.symbol,
amount: cosmosIBCTransferContent.amount,
toAddress: cosmosIBCTransferContent.toAddress,
targetChainName: cosmosIBCTransferContent.targetChainName,
};

try {
const walletProvider: ICosmosWalletChains =
await initWalletChainsData(_runtime);

const action = new CosmosIBCTransferAction(walletProvider);

const customAssets = (pluginOptions?.customChainData ?? []).map(
(chainData) => chainData.assets
);

const transferResp = await action.execute(
paramOptions,
bridgeDataProvider,
customAssets
);

if (_callback) {
await _callback({
text: `Successfully transferred ${paramOptions.amount} tokens from ${paramOptions.chainName} to ${paramOptions.toAddress} on ${paramOptions.targetChainName}\nGas paid: ${transferResp.gasPaid}\nTransaction Hash: ${transferResp.txHash}`,
content: {
success: true,
hash: transferResp.txHash,
amount: paramOptions.amount,
recipient: transferResp.to,
fromChain: paramOptions.chainName,
toChain: paramOptions.targetChainName,
},
});

const newMemory: Memory = {
userId: _message.agentId,
agentId: _message.agentId,
roomId: _message.roomId,
content: {
text: `Transaction ${paramOptions.amount} ${paramOptions.symbol} to address ${paramOptions.toAddress} from chain ${paramOptions.chainName} to ${paramOptions.targetChainName} was successfully transferred.\n Gas paid: ${transferResp.gasPaid}. Tx hash: ${transferResp.txHash}`,
},
};

await _runtime.messageManager.createMemory(newMemory);
}
return true;
} catch (error) {
console.error("Error during ibc token transfer:", error);

if (_callback) {
await _callback({
text: `Error ibc transferring tokens: ${error.message}`,
content: { error: error.message },
});
}

const newMemory: Memory = {
userId: _message.agentId,
agentId: _message.agentId,
roomId: _message.roomId,
content: {
text: `Transaction ${paramOptions.amount} ${paramOptions.symbol} to address ${paramOptions.toAddress} on chain ${paramOptions.chainName} to ${paramOptions.targetChainName} was unsuccessful.`,
},
};

await _runtime.messageManager.createMemory(newMemory);

return false;
}
},
template: cosmosTransferTemplate,
validate: async (runtime: IAgentRuntime) => {
const mnemonic = runtime.getSetting("COSMOS_RECOVERY_PHRASE");
const availableChains = runtime.getSetting("COSMOS_AVAILABLE_CHAINS");
const availableChainsArray = availableChains?.split(",");

return !(mnemonic && availableChains && availableChainsArray.length);
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Make an IBC transfer {{0.0001 ATOM}} to {{osmosis1pcnw46km8m5amvf7jlk2ks5std75k73aralhcf}} from {{cosmoshub}} to {{osmosis}}",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "Do you confirm the IBC transfer action?",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user1}}",
content: {
text: "Yes",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "",
action: "COSMOS_IBC_TRANSFER",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Send {{50 OSMO}} to {{juno13248w8dtnn07sxc3gq4l3ts4rvfyat6f4qkdd6}} from {{osmosis}} to {{juno}}",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "Do you confirm the IBC transfer action?",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user1}}",
content: {
text: "Yes",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "",
action: "COSMOS_IBC_TRANSFER",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Transfer {{0.005 JUNO}} from {{juno}} to {{cosmos1n0xv7z2pkl4eppnm7g2rqhe2q8q6v69h7w93fc}} on {{cosmoshub}}",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "Do you confirm the IBC transfer action?",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user1}}",
content: {
text: "Yes",
action: "COSMOS_IBC_TRANSFER",
},
},
{
user: "{{user2}}",
content: {
text: "",
action: "COSMOS_IBC_TRANSFER",
},
},
],
],
similes: [
"COSMOS_TRANSFER",
"COSMOS_SEND_TOKENS",
"COSMOS_TOKEN_TRANSFER",
"COSMOS_MOVE_TOKENS",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that these similes should be unique between actions but some of them are the same as for COSMOS_TRANSFER action (action from first PR).

],
});
37 changes: 37 additions & 0 deletions packages/plugin-cosmos/src/actions/ibc-transfer/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from "zod";

export const IBCTransferParamsSchema = z.object({
chainName: z.string(),
symbol: z.string(),
amount: z.string(),
toAddress: z.string(),
targetChainName: z.string(),
});

export const bridgeDataProviderParamsSchema = z.object({
source_asset_denom: z.string(),
source_asset_chain_id: z.string(),
allow_multi_tx: z.boolean(),
});

export const bridgeDataProviderResponseAssetsSchema = z.object({
denom: z.string(),
chain_id: z.string(),
origin_denom: z.string(),
origin_chain_id: z.string(),
trace: z.string(),
symbol: z.string().optional(),
name: z.string().optional(),
logo_uri: z.string().optional(),
decimals: z.number().optional(),
recommended_symbol: z.string().optional(),
});

export const bridgeDataProviderResponseSchema = z.object({
dest_assets: z.record(
z.string(),
z.object({
assets: z.array(bridgeDataProviderResponseAssetsSchema),
})
),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { bridgeDataProviderResponseSchema } from "../schema";
import { BridgeDataProviderParams, BridgeDataProviderResponse } from "../types";
import axios from "axios";

type CacheKey = `${string}_${string}`;

export class BridgeDataFetcher {
private static instance: BridgeDataFetcher;
private cache: Map<CacheKey, BridgeDataProviderResponse>;
private readonly apiUrl: string;

private constructor() {
this.cache = new Map();
this.apiUrl = "https://api.skip.build/v2/fungible/assets_from_source";
}

public static getInstance(): BridgeDataFetcher {
if (!BridgeDataFetcher.instance) {
BridgeDataFetcher.instance = new BridgeDataFetcher();
}
return BridgeDataFetcher.instance;
}

private generateCacheKey(
sourceAssetDenom: string,
sourceAssetChainId: string
): CacheKey {
return `${sourceAssetDenom}_${sourceAssetChainId}`;
}

public async fetchBridgeData(
sourceAssetDenom: string,
sourceAssetChainId: string
): Promise<BridgeDataProviderResponse> {
const cacheKey = this.generateCacheKey(
sourceAssetDenom,
sourceAssetChainId
);

if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}

const requestData: BridgeDataProviderParams = {
source_asset_denom: sourceAssetDenom,
source_asset_chain_id: sourceAssetChainId,
allow_multi_tx: false,
};

try {
const response = await axios.post(this.apiUrl, requestData, {
headers: {
"Content-Type": "application/json",
},
});

const validResponse = bridgeDataProviderResponseSchema.parse(
response.data
);

this.cache.set(cacheKey, validResponse);
return response.data;
} catch (error) {
console.error("Error fetching assets:", error);
throw error;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IBridgeDataProvider } from "../../../shared/interfaces";
import { BridgeDataFetcher } from "./bridge-data-fetcher";

export const bridgeDataProvider: IBridgeDataProvider = async (
sourceAssetDenom: string,
sourceAssetChainId: string
) => {
const bridgeDataFetcher = BridgeDataFetcher.getInstance();
const bridgeData = await bridgeDataFetcher.fetchBridgeData(
sourceAssetDenom,
sourceAssetChainId
);

const ibcAssetData = bridgeData.dest_assets[
sourceAssetChainId
]?.assets?.find(({ origin_denom }) => origin_denom === sourceAssetDenom);

if (!ibcAssetData) {
throw new Error("No IBC asset data");
}

const channelId = ibcAssetData.trace.split("/")[0];

if (!channelId) {
throw new Error("No channel for bridge");
}

return {
channelId,
ibcDenom: ibcAssetData.denom,
portId: "transfer",
};
};
Loading
Loading