Skip to content

Commit 2c4b037

Browse files
committed
feat: add bridge data fetcher and update tests
1 parent b3d8b05 commit 2c4b037

15 files changed

+503
-277
lines changed

packages/plugin-cosmos/src/actions/ibc-transfer/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import type {
1818
} from "../../shared/interfaces";
1919
import { IBCTransferActionParams } from "./types";
2020
import { IBCTransferAction } from "./services/ibc-transfer-action-service";
21+
import { bridgeDenomProvider } from "./services/bridge-denom-provider";
2122

2223
export const createIBCTransferAction = (
2324
pluginOptions: ICosmosPluginOptions
2425
) => ({
2526
name: "COSMOS_IBC_TRANSFER",
26-
description: "Transfer tokens between addresses on cosmos chains",
27+
description: "Transfer tokens between addresses on cosmos chains",
2728
handler: async (
2829
_runtime: IAgentRuntime,
2930
_message: Memory,
@@ -63,12 +64,13 @@ export const createIBCTransferAction = (
6364

6465
const transferResp = await action.execute(
6566
paramOptions,
67+
bridgeDenomProvider,
6668
customAssets
6769
);
6870

6971
if (_callback) {
7072
await _callback({
71-
text: `Successfully transferred ${paramOptions.amount} tokens from ${paramOptions.chainName} to ${paramOptions.toAddress} on ${paramOptions.targetChainName}\nGas paid: ${transferResp.gasPaid}\nTransaction Hash: ${transferResp.txHash}`,
73+
text: `Successfully transferred ${paramOptions.amount} tokens from ${paramOptions.chainName} to ${paramOptions.toAddress} on ${paramOptions.targetChainName}\nTransaction Hash: ${transferResp.txHash}`,
7274
content: {
7375
success: true,
7476
hash: transferResp.txHash,
@@ -84,7 +86,7 @@ export const createIBCTransferAction = (
8486
agentId: _message.agentId,
8587
roomId: _message.roomId,
8688
content: {
87-
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}`,
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}`,
8890
},
8991
};
9092

packages/plugin-cosmos/src/actions/ibc-transfer/schema.ts

+28
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,31 @@ export const IBCTransferParamsSchema = z.object({
77
toAddress: z.string(),
88
targetChainName: z.string(),
99
});
10+
11+
export const bridgeDataProviderParamsSchema = z.object({
12+
source_asset_denom: z.string(),
13+
source_asset_chain_id: z.string(),
14+
allow_multi_tx: z.boolean(),
15+
});
16+
17+
export const bridgeDataProviderResponseAssetsSchema = z.object({
18+
denom: z.string(),
19+
chain_id: z.string(),
20+
origin_denom: z.string(),
21+
origin_chain_id: z.string(),
22+
trace: z.string(),
23+
symbol: z.string().optional(),
24+
name: z.string().optional(),
25+
logo_uri: z.string().optional(),
26+
decimals: z.number().optional(),
27+
recommended_symbol: z.string().optional(),
28+
});
29+
30+
export const bridgeDataProviderResponseSchema = z.object({
31+
dest_assets: z.record(
32+
z.string(),
33+
z.object({
34+
assets: z.array(bridgeDataProviderResponseAssetsSchema),
35+
})
36+
),
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IDenomProvider } from "../../../shared/interfaces";
2+
import { BridgeDataFetcher } from "../../../shared/services/bridge-data-fetcher";
3+
4+
export const bridgeDenomProvider: IDenomProvider = async (
5+
sourceAssetDenom: string,
6+
sourceAssetChainId: string,
7+
destChainId: string
8+
) => {
9+
const bridgeDataFetcher = BridgeDataFetcher.getInstance();
10+
const bridgeData = await bridgeDataFetcher.fetchBridgeData(
11+
sourceAssetDenom,
12+
sourceAssetChainId
13+
);
14+
15+
const ibcAssetData = bridgeData.dest_assets[destChainId]?.assets?.find(
16+
({ origin_denom }) => origin_denom === sourceAssetDenom
17+
);
18+
19+
if (!ibcAssetData.denom) {
20+
throw new Error("No IBC asset data");
21+
}
22+
23+
return {
24+
denom: ibcAssetData.denom,
25+
};
26+
};

packages/plugin-cosmos/src/actions/ibc-transfer/services/ibc-transfer-action-service.ts

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {
2+
convertDisplayUnitToBaseUnit,
23
getAssetBySymbol,
4+
getChainByChainId,
35
getChainByChainName,
4-
getChainIdByChainName,
56
} from "@chain-registry/utils";
67
import { assets, chains } from "chain-registry";
78
import type {
9+
IDenomProvider,
810
ICosmosActionService,
911
ICosmosPluginCustomChainData,
1012
ICosmosTransaction,
@@ -20,6 +22,7 @@ export class IBCTransferAction implements ICosmosActionService {
2022

2123
async execute(
2224
params: IBCTransferActionParams,
25+
bridgeDenomProvider: IDenomProvider,
2326
customChainAssets?: ICosmosPluginCustomChainData["assets"][]
2427
): Promise<ICosmosTransaction> {
2528
const senderAddress = await this.cosmosWalletChains.getWalletAddress(
@@ -66,6 +69,7 @@ export class IBCTransferAction implements ICosmosActionService {
6669
if (!denom.base) {
6770
throw new Error("Cannot find asset");
6871
}
72+
6973
if (!sourceChain) {
7074
throw new Error("Cannot find source chain");
7175
}
@@ -74,23 +78,34 @@ export class IBCTransferAction implements ICosmosActionService {
7478
throw new Error("Cannot find destination chain");
7579
}
7680

81+
const { denom: destAssetDenom } = await bridgeDenomProvider(
82+
denom.base,
83+
sourceChain.chain_id,
84+
destChain.chain_id
85+
);
86+
7787
const route = await skipClient.route({
7888
destAssetChainID: destChain.chain_id,
79-
destAssetDenom: denom.base,
89+
destAssetDenom,
8090
sourceAssetChainID: sourceChain.chain_id,
8191
sourceAssetDenom: denom.base,
82-
amountOut: params.amount,
92+
amountIn: convertDisplayUnitToBaseUnit(
93+
availableAssets,
94+
params.symbol,
95+
params.amount,
96+
params.chainName
97+
),
98+
cumulativeAffiliateFeeBPS: "0",
8399
});
84100

85101
const userAddresses = await Promise.all(
86102
route.requiredChainAddresses.map(async (chainID) => {
87-
const chainName = getChainIdByChainName(chains, chainID);
103+
const chain = getChainByChainId(chains, chainID);
88104
return {
89105
chainID,
90-
address:
91-
await this.cosmosWalletChains.getWalletAddress(
92-
chainName
93-
),
106+
address: await this.cosmosWalletChains.getWalletAddress(
107+
chain.chain_name
108+
),
94109
};
95110
})
96111
);
@@ -104,7 +119,6 @@ export class IBCTransferAction implements ICosmosActionService {
104119
txHash = executeRouteTxHash;
105120
},
106121
});
107-
108122
return {
109123
from: senderAddress,
110124
to: params.toAddress,
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
import { z } from "zod";
2-
import { IBCTransferParamsSchema } from "./schema";
2+
import {
3+
bridgeDataProviderParamsSchema,
4+
bridgeDataProviderResponseAssetsSchema,
5+
bridgeDataProviderResponseSchema,
6+
IBCTransferParamsSchema,
7+
} from "./schema";
38

49
export type IBCTransferActionParams = z.infer<typeof IBCTransferParamsSchema>;
10+
export type BridgeDataProviderParams = z.infer<
11+
typeof bridgeDataProviderParamsSchema
12+
>;
13+
export type BridgeDataProviderResponseAsset = z.infer<
14+
typeof bridgeDataProviderResponseAssetsSchema
15+
>;
16+
export type BridgeDataProviderResponse = z.infer<
17+
typeof bridgeDataProviderResponseSchema
18+
>;

packages/plugin-cosmos/src/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createTransferAction } from "./actions/transfer";
22
import type { Plugin } from "@elizaos/core";
33
import { createCosmosWalletProvider } from "./providers/wallet";
44
import { ICosmosPluginOptions } from "./shared/interfaces";
5+
import { createIBCTransferAction } from "./actions/ibc-transfer";
56

67
export const createCosmosPlugin = (
78
pluginOptions?: ICosmosPluginOptions
@@ -11,7 +12,10 @@ export const createCosmosPlugin = (
1112
providers: [createCosmosWalletProvider(pluginOptions)],
1213
evaluators: [],
1314
services: [],
14-
actions: [createTransferAction(pluginOptions)],
15+
actions: [
16+
createTransferAction(pluginOptions),
17+
createIBCTransferAction(pluginOptions),
18+
],
1519
});
1620

1721
export default createCosmosPlugin;

packages/plugin-cosmos/src/providers/wallet/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IAgentRuntime } from "@ai16z/eliza";
1+
import { IAgentRuntime } from "@elizaos/core";
22
import {
33
convertBaseUnitToDisplayUnit,
44
getSymbolByDenom,

packages/plugin-cosmos/src/shared/entities/cosmos-wallet-chains-data.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,25 @@ export class CosmosWalletChains implements ICosmosWalletChains {
6565
}
6666

6767
public async getWalletAddress(chainName: string) {
68-
return await this.walletChainsData[chainName].wallet.getWalletAddress();
68+
const chainWalletsForGivenChain = this.walletChainsData[chainName];
69+
if (!chainWalletsForGivenChain) {
70+
throw new Error("Invalid chain name");
71+
}
72+
73+
return await chainWalletsForGivenChain.wallet.getWalletAddress();
6974
}
7075

7176
public getSigningCosmWasmClient(chainName: string) {
7277
return this.walletChainsData[chainName].signingCosmWasmClient;
7378
}
7479

7580
public getSkipClient(chainName: string): SkipClient {
76-
return this.walletChainsData[chainName].skipClient;
77-
}
81+
const chainWalletsForGivenChain = this.walletChainsData[chainName];
82+
83+
if (!chainWalletsForGivenChain) {
84+
throw new Error("Invalid chain name");
85+
}
7886

79-
public async getUserAddress(chainName: string): Promise<string> {
80-
return this.walletChainsData[chainName].wallet.getWalletAddress();
87+
return chainWalletsForGivenChain.skipClient;
8188
}
8289
}

packages/plugin-cosmos/src/shared/interfaces.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ export interface ICosmosWalletChainsData {
4848
[chainName: string]: ICosmosChainWallet;
4949
}
5050

51-
export interface IBridgeDataProvider {
51+
export interface IDenomProvider {
5252
(
5353
sourceAssetDenom: string,
54-
sourceAssetChainId: string
55-
): Promise<{ channelId: string; portId: string; ibcDenom: string }>;
54+
sourceAssetChainId: string,
55+
destChainId: string
56+
): Promise<{ denom: string }>;
5657
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { bridgeDataProviderResponseSchema } from "../../actions/ibc-transfer/schema";
2+
import {
3+
BridgeDataProviderParams,
4+
BridgeDataProviderResponse,
5+
} from "../../actions/ibc-transfer/types";
6+
import axios from "axios";
7+
8+
type CacheKey = `${string}_${string}`;
9+
10+
export class BridgeDataFetcher {
11+
private static instance: BridgeDataFetcher;
12+
private cache: Map<CacheKey, BridgeDataProviderResponse>;
13+
private readonly apiUrl: string;
14+
15+
private constructor() {
16+
this.cache = new Map();
17+
this.apiUrl = "https://api.skip.build/v2/fungible/assets_from_source";
18+
}
19+
20+
public static getInstance(): BridgeDataFetcher {
21+
if (!BridgeDataFetcher.instance) {
22+
BridgeDataFetcher.instance = new BridgeDataFetcher();
23+
}
24+
return BridgeDataFetcher.instance;
25+
}
26+
27+
private generateCacheKey(
28+
sourceAssetDenom: string,
29+
sourceAssetChainId: string
30+
): CacheKey {
31+
return `${sourceAssetDenom}_${sourceAssetChainId}`;
32+
}
33+
34+
public async fetchBridgeData(
35+
sourceAssetDenom: string,
36+
sourceAssetChainId: string
37+
): Promise<BridgeDataProviderResponse> {
38+
const cacheKey = this.generateCacheKey(
39+
sourceAssetDenom,
40+
sourceAssetChainId
41+
);
42+
43+
if (this.cache.has(cacheKey)) {
44+
return this.cache.get(cacheKey)!;
45+
}
46+
47+
const requestData: BridgeDataProviderParams = {
48+
source_asset_denom: sourceAssetDenom,
49+
source_asset_chain_id: sourceAssetChainId,
50+
allow_multi_tx: false,
51+
};
52+
53+
try {
54+
const response = await axios.post(this.apiUrl, requestData, {
55+
headers: {
56+
"Content-Type": "application/json",
57+
},
58+
});
59+
60+
const validResponse = bridgeDataProviderResponseSchema.parse(
61+
response.data
62+
);
63+
64+
this.cache.set(cacheKey, validResponse);
65+
return response.data;
66+
} catch (error) {
67+
console.error("Error fetching assets:", error);
68+
throw error;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)