Skip to content

Commit 91f1b6a

Browse files
authored
Feat/hydra (#64)
+ Swap tokens on Hydra + Swap cross-chain tokens by 1 click + Add tests cases
1 parent 7ab06d3 commit 91f1b6a

File tree

27 files changed

+9518
-3957
lines changed

27 files changed

+9518
-3957
lines changed

examples/telegram-bot/src/TelegramBot.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ export class TelegramBot {
6060
// xcm_transfer_native_asset
6161
const xcmTransferNativeAsset = this.agent.xcmTransferNativeTool();
6262

63+
const swapTokens = this.agent.swapTokensTool();
64+
6365
setupHandlers(this.bot, this.llm, {
6466
checkBalance: checkBalance,
6567
transferNative: transferNative,
6668
xcmTransferNativeAsset: xcmTransferNativeAsset,
69+
swapTokens: swapTokens,
6770
});
6871

6972
console.log("Bot initialization complete");

examples/telegram-bot/src/handlers.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import { HumanMessage, SystemMessage } from "@langchain/core/messages";
33
import { DynamicStructuredTool } from "@langchain/core/tools";
44
import { ChatModelWithTools } from "./models";
55

6-
const SYSTEM_PROMPT = `I am a Telegram bot powered by PolkadotAgentKit. I can assist you with:
6+
export const SYSTEM_PROMPT = `I am a Telegram bot powered by PolkadotAgentKit. I can assist you with:
77
- Transferring native tokens on specific chain (e.g., "transfer 1 WND to 5CSox4ZSN4SGLKUG9NYPtfVK9sByXLtxP4hmoF4UgkM4jgDJ on westend_asset_hub")
88
- Checking WND balance on Westend (e.g., "check balance")
99
- Checking proxies (e.g., "check proxies on westend" or "check proxies")
1010
- Transfer tokens through XCM (e.g., "transfer 1 WND to 5CSox4ZSN4SGLKUG9NYPtfVK9sByXLtxP4hmoF4UgkM4jgDJ from west to westend_asset_hub ")
1111
12-
CHAIN NAME CONVERSION RULES for checking balance and transfer native tokens on specific chain : When users mention chain names in checking balance and transfer native tokens on specific chain , I must convert them to the correct parameter values using this mapping:
12+
DYNAMIC CHAIN INITIALIZATION:
13+
When balance checking, native transfers, or XCM transfer tools fail because a chain is not available/initialized, I should:
14+
1. Use initializeChainApiTool to initialize the missing chain
15+
2. Retry the original operation
1316
14-
| User Input | Real Param (USE THIS IN TOOL CALLS) |
15-
|------------|-------------------------------------|
16-
| Westend | west |
17-
| Westend Asset Hub | west_asset_hub |
18-
| Polkadot | polkadot |
19-
| Kusama | kusama |
20-
| AssetHubWestend | west_asset_hub |
21-
| AssetHubPolkadot | polkadot_asset_hub |
17+
CRITICAL: SWAP OPERATIONS USE DIFFERENT CHAIN NAME FORMAT!
18+
19+
CHAIN NAME CONVERSION RULES for SWAP OPERATIONS ONLY:
20+
When using swapTokensTool, I MUST use PascalCase format:
21+
22+
| User Input | Real Param for SWAP (USE THIS IN swapTokensTool) |
23+
|------------|--------------------------------------------------|
24+
| polkadot | Polkadot |
25+
| dot | Polkadot |
26+
| Polkadot | Polkadot |
27+
| asset hub | AssetHubPolkadot |
28+
| polkadot asset hub | AssetHubPolkadot |
29+
| AssetHubPolkadot | AssetHubPolkadot |
30+
| Polkadot Asset Hub | AssetHubPolkadot |
31+
| Hydra | Hydra |
32+
| hydra | Hydra |
33+
| Kusama | Kusama |
34+
| kusama | Kusama |
2235
2336
2437
CHAIN NAME CONVERSION RULES for transfer tokens through XCM: When users mention chain names in transfer tokens through XCM, I must convert them to the correct parameter values using this mapping:
@@ -33,6 +46,22 @@ CHAIN NAME CONVERSION RULES for transfer tokens through XCM: When users mention
3346
| Polkadot Asset Hub | polkadot_asset_hub |
3447
3548
49+
CHAIN NAME CONVERSION RULES for swap tokens (when users mention chain names in swap): When users mention chain names in swap, I must convert them to the correct parameter values using this mapping:
50+
51+
| User Input | Real Param (USE THIS IN TOOL CALLS) |
52+
|------------|-------------------------------------|
53+
| dot | Polkadot |
54+
| asset hub | AssetHubPolkadot |
55+
| polkadot | Polkadot |
56+
| Polkadot | Polkadot |
57+
| AssetHubPolkadot | AssetHubPolkadot |
58+
| Polkadot Asset Hub | AssetHubPolkadot |
59+
| Hydra | Hydra |
60+
| Kusama | Kusama |
61+
62+
63+
64+
3665
For XCM transfers, when the user says:
3766
"transfer X WND to [address] from [source_chain_user_input] to [dest_chain_user_input]"
3867
@@ -56,6 +85,37 @@ When transferring tokens through XCM, please provide:
5685
3. The name of the source chain (convert to real param)
5786
4. The name of the destination chain (convert to real param)
5887
88+
When swapping tokens cross-chain, please provide:
89+
1. The amount of tokens to swap (e.g., 1)
90+
2. The symbol of the token to swap from (e.g., 'DOT', 'KSM', 'HDX')
91+
3. The symbol of the token to swap to (e.g., 'DOT', 'KSM', 'HDX', 'USDT')
92+
4. The name of the source chain (convert to real param)
93+
5. The name of the destination chain (convert to real param)
94+
6. The receiver address (optional - if not provided, defaults to sender)
95+
96+
When swapping tokens DEX-specific, please provide:
97+
1. The amount of tokens to swap (e.g., 1)
98+
2. The symbol of the token to swap from (e.g., 'DOT', 'KSM', 'HDX')
99+
3. The symbol of the token to swap to (e.g., 'DOT', 'KSM', 'HDX', 'USDT')
100+
4. The name of the DEX (e.g., 'HydrationDex', default is 'HydrationDex' if dex is not provided)
101+
5. The receiver address (optional - if not provided, defaults to sender)
102+
103+
Note: The sender address is handled automatically by the system.
104+
105+
For example cross-chain swap:
106+
User: "swap 0.1 DOT from Polkadot to USDT on Hydra"
107+
Tool call should use: from: "Polkadot", to: "Hydra", currencyFrom: "DOT", currencyTo: "USDt", amount: "0.1"
108+
109+
User: "swap 0.1 DOT from Polkadot to USDT on Hydra to 5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr"
110+
Tool call should use: from: "Polkadot", to: "Hydra", currencyFrom: "DOT", currencyTo: "USDt", amount: "0.1", receiver: "5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr"
111+
112+
For example DEX-specific swap:
113+
User: "swap 0.1 DOT to USDT to 5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr on HydrationDex"
114+
Tool call should use: currencyFrom: "DOT", currencyTo: "USDT", amount: "0.1", dex: "HydrationDex", receiver: "5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr"
115+
116+
User: "swap 0.1 DOT to USDT on HydrationDex to 5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr"
117+
Tool call should use: currencyFrom: "DOT", currencyTo: "USDT", amount: "0.1", dex: "HydrationDex", receiver: "5D7jcv6aYbhbYGVY8k65oemM6FVNoyBfoVkuJ5cbFvbefftr"
118+
59119
When checking proxies, you can specify the chain (convert to real param) or not specify a chain (the first chain will be used by default)
60120
61121
Please provide instructions, and I will assist you!`;

packages/core/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
"test:watch": "vitest"
3636
},
3737
"dependencies": {
38-
"@paraspell/sdk": "^10.10.1",
38+
"@paraspell/sdk": "^10.10.7",
3939
"@polkadot-agent-kit/common": "workspace:*",
4040
"@subsquid/ss58": "^2.0.2",
4141
"polkadot-api": "^1.9.13",
42-
"rxjs": "^7.8.2"
42+
"rxjs": "^7.8.2",
43+
"@paraspell/xcm-router": "^10.10.3",
44+
"@paraspell/assets": "^10.10.3"
4345
},
4446
"devDependencies": {
4547
"@babel/plugin-syntax-import-attributes": "^7.26.0",
@@ -52,6 +54,7 @@
5254
"dotenv": "^16.4.7",
5355
"prettier": "^3.5.3",
5456
"rollup": "^4.37.0",
55-
"rollup-plugin-dts": "^6.2.1"
57+
"rollup-plugin-dts": "^6.2.1",
58+
"@galacticcouncil/api-augment": "^0.3.0"
5659
}
5760
}

packages/core/src/defi/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./swap"

packages/core/src/defi/swap.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { getAssetDecimals, getAssetMultiLocation } from "@paraspell/assets"
2+
import type { TCurrency, TMultiLocation, TNodeDotKsmWithRelayChains } from "@paraspell/sdk"
3+
import type {
4+
RouterBuilderCore,
5+
TBuildTransactionsOptions,
6+
TExchangeInput,
7+
TRouterAsset,
8+
TRouterPlan
9+
} from "@paraspell/xcm-router"
10+
import { RouterBuilder } from "@paraspell/xcm-router"
11+
import { parseUnits } from "@polkadot-agent-kit/common"
12+
import type { PolkadotSigner } from "polkadot-api/signer"
13+
14+
import { getPairSupported } from "../utils/defi"
15+
16+
// Constants
17+
const DEFAULT_SLIPPAGE_PCT = "1"
18+
const HYDRATION_DEX = "HydrationDex"
19+
20+
export interface SwapTokenArgs {
21+
from?: string
22+
to?: string
23+
currencyFrom: string
24+
currencyTo: string
25+
amount: string
26+
sender?: string
27+
receiver?: string
28+
dex?: string
29+
}
30+
31+
/**
32+
* Builds a token swap transaction supporting both cross-chain and DEX-specific swaps.
33+
*
34+
* This function uses the \@paraspell/xcm-router RouterBuilder to construct token swaps
35+
* that exchange one token for another within the Polkadot ecosystem.
36+
*
37+
* **Two supported modes:**
38+
* 1. **Cross-chain swap**: Uses XCM routing between different chains via Hydration DEX
39+
* 2. **DEX-specific swap**: Direct swap within a specific DEX (e.g., HydrationDex)
40+
*
41+
* @param args - The swap configuration object
42+
* @param signer - The Polkadot signer for transaction signing
43+
* @param isCrossChainSwap - Boolean flag to determine swap type
44+
* @returns A Promise resolving to a TRouterPlan object containing the swap transaction plan
45+
*/
46+
export const swapTokens = async (
47+
args: SwapTokenArgs,
48+
signer: PolkadotSigner,
49+
isCrossChainSwap: boolean
50+
): Promise<TRouterPlan> => {
51+
validateSwapArgs(args, isCrossChainSwap)
52+
53+
return isCrossChainSwap
54+
? await executeCrossChainSwap(args, signer)
55+
: await executeDexSwap(args, signer)
56+
}
57+
58+
/**
59+
* Validates swap arguments based on swap type
60+
*/
61+
function validateSwapArgs(args: SwapTokenArgs, isCrossChainSwap: boolean): void {
62+
if (isCrossChainSwap) {
63+
if (!args.from || !args.to) {
64+
throw new Error("Cross-chain swaps require both 'from' and 'to' chain parameters")
65+
}
66+
} else {
67+
if (!args.dex) {
68+
throw new Error("DEX-specific swaps require 'dex' parameter")
69+
}
70+
}
71+
72+
if (!args.currencyFrom || !args.currencyTo) {
73+
throw new Error("Both 'currencyFrom' and 'currencyTo' are required")
74+
}
75+
76+
if (!args.amount || parseFloat(args.amount) <= 0) {
77+
throw new Error("Amount must be a positive number")
78+
}
79+
}
80+
81+
/**
82+
* Executes cross-chain swap using XCM routing
83+
*/
84+
async function executeCrossChainSwap(
85+
args: SwapTokenArgs,
86+
signer: PolkadotSigner
87+
): Promise<TRouterPlan> {
88+
const { multilocationFrom, multilocationTo } = getCrossChainMultilocations(args)
89+
if (!multilocationFrom || !multilocationTo) {
90+
throw new Error("Failed to get multilocations for cross-chain swap")
91+
}
92+
const formattedAmount = formatCrossChainAmount(args)
93+
94+
// Validate fees before proceeding
95+
await validateSwapFees({
96+
builder: createCrossChainRouterBuilder(
97+
args,
98+
multilocationFrom,
99+
multilocationTo,
100+
formattedAmount
101+
),
102+
swapType: "cross-chain"
103+
})
104+
105+
return await createCrossChainRouterBuilder(
106+
args,
107+
multilocationFrom,
108+
multilocationTo,
109+
formattedAmount
110+
)
111+
.signer(signer)
112+
.buildTransactions()
113+
}
114+
115+
/**
116+
* Executes DEX-specific swap
117+
*/
118+
async function executeDexSwap(args: SwapTokenArgs, signer: PolkadotSigner): Promise<TRouterPlan> {
119+
const { currencyFrom, currencyTo } = validateAndGetDexPair(args)
120+
const decimals = getAssetDecimals("Hydration", args.currencyFrom)
121+
if (!decimals) {
122+
throw new Error(`Failed to get decimals for ${args.currencyFrom} on Hydration`)
123+
}
124+
125+
const formattedAmount = parseUnits(args.amount, decimals)
126+
127+
// Validate fees before proceeding
128+
await validateSwapFees({
129+
builder: createDexRouterBuilder(
130+
args,
131+
currencyFrom,
132+
currencyTo,
133+
BigInt(formattedAmount).toString()
134+
),
135+
swapType: "DEX-specific"
136+
})
137+
138+
return await createDexRouterBuilder(
139+
args,
140+
currencyFrom,
141+
currencyTo,
142+
BigInt(formattedAmount).toString()
143+
)
144+
.signer(signer)
145+
.buildTransactions()
146+
}
147+
148+
/**
149+
* Gets multilocations for cross-chain currencies
150+
*/
151+
function getCrossChainMultilocations(args: SwapTokenArgs) {
152+
const multilocationFrom = getAssetMultiLocation(args.from as TNodeDotKsmWithRelayChains, {
153+
symbol: args.currencyFrom
154+
})
155+
156+
const multilocationTo = getAssetMultiLocation(args.to as TNodeDotKsmWithRelayChains, {
157+
symbol: args.currencyTo
158+
})
159+
160+
return { multilocationFrom, multilocationTo }
161+
}
162+
163+
/**
164+
* Formats amount for cross-chain swaps using proper decimals
165+
*/
166+
function formatCrossChainAmount(args: SwapTokenArgs): string {
167+
const decimals = getAssetDecimals(args.from as TNodeDotKsmWithRelayChains, args.currencyFrom)
168+
169+
if (!decimals) {
170+
throw new Error(`Failed to get decimals for ${args.currencyFrom} on ${args.from}`)
171+
}
172+
173+
return parseUnits(args.amount, decimals).toString()
174+
}
175+
176+
/**
177+
* Validates DEX pair support and returns currency objects
178+
*/
179+
function validateAndGetDexPair(args: SwapTokenArgs) {
180+
const pair = getPairSupported(args.currencyFrom, args.currencyTo, args.dex)
181+
182+
if (!pair) {
183+
throw new Error(
184+
`Trading pair ${args.currencyFrom}/${args.currencyTo} is not supported on ${args.dex}`
185+
)
186+
}
187+
188+
return {
189+
currencyFrom: pair[0],
190+
currencyTo: pair[1]
191+
}
192+
}
193+
194+
/**
195+
* Creates router builder for cross-chain swaps
196+
*/
197+
function createCrossChainRouterBuilder(
198+
args: SwapTokenArgs,
199+
multilocationFrom: TMultiLocation,
200+
multilocationTo: TMultiLocation,
201+
formattedAmount: string
202+
) {
203+
return RouterBuilder()
204+
.from(args.from as TNodeDotKsmWithRelayChains)
205+
.to(args.to as TNodeDotKsmWithRelayChains)
206+
.exchange(HYDRATION_DEX)
207+
.currencyFrom({ multilocation: multilocationFrom })
208+
.currencyTo({ multilocation: multilocationTo })
209+
.amount(BigInt(formattedAmount).toString())
210+
.slippagePct(DEFAULT_SLIPPAGE_PCT)
211+
.senderAddress(args.sender || "")
212+
.recipientAddress(args.receiver || "")
213+
}
214+
215+
/**
216+
* Creates router builder for DEX-specific swaps
217+
*/
218+
function createDexRouterBuilder(
219+
args: SwapTokenArgs,
220+
currencyFrom: TRouterAsset,
221+
currencyTo: TRouterAsset,
222+
formattedAmount: string
223+
) {
224+
return RouterBuilder()
225+
.exchange(args.dex as TExchangeInput)
226+
.currencyFrom({ id: currencyFrom.assetId as TCurrency })
227+
.currencyTo({ id: currencyTo.assetId as TCurrency })
228+
.amount(formattedAmount)
229+
.slippagePct(DEFAULT_SLIPPAGE_PCT)
230+
.senderAddress(args.sender || "")
231+
.recipientAddress(args.receiver || "")
232+
}
233+
234+
/**
235+
* Validates swap fees before execution
236+
*/
237+
async function validateSwapFees({
238+
builder,
239+
swapType
240+
}: {
241+
builder: RouterBuilderCore<TBuildTransactionsOptions>
242+
swapType: "cross-chain" | "DEX-specific"
243+
}): Promise<void> {
244+
const fees = await builder.getXcmFees()
245+
246+
if (fees.failureChain || fees.failureReason) {
247+
throw new Error(
248+
`Failed to calculate ${swapType} swap fees: ${fees.failureChain || fees.failureReason}`
249+
)
250+
}
251+
}

0 commit comments

Comments
 (0)