@elizaos/plugin-wallet 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/auto-enable.ts +76 -0
- package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
- package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
- package/dist/aerodrome-CfnESC32.mjs +890 -0
- package/dist/chunk-hT5z_Zn9.mjs +35 -0
- package/dist/index.d.mts +34727 -0
- package/dist/index.mjs +21590 -0
- package/dist/lib/server-wallet-trade.d.mts +34 -0
- package/dist/lib/server-wallet-trade.mjs +306 -0
- package/dist/meteora-BPX39hZo.mjs +22640 -0
- package/dist/orca-Bybp1HXO.mjs +249 -0
- package/dist/pancakeswp-CkEXlXti.mjs +604 -0
- package/dist/plugin-ZO_MTyd0.mjs +529 -0
- package/dist/raydium-rfaM9yEf.mjs +539 -0
- package/dist/sdk/index.d.mts +32492 -0
- package/dist/sdk/index.mjs +6415 -0
- package/dist/types-D5252NZk.mjs +487 -0
- package/dist/uniswap-CReXgXVN.mjs +573 -0
- package/dist/wallet-action.d.mts +6 -0
- package/dist/wallet-action.mjs +820 -0
- package/package.json +152 -0
- package/src/actions/failure-codes.ts +79 -0
- package/src/actions/index.ts +1 -0
- package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
- package/src/analytics/birdeye/birdeye-task.ts +175 -0
- package/src/analytics/birdeye/birdeye.ts +813 -0
- package/src/analytics/birdeye/constants.ts +74 -0
- package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
- package/src/analytics/birdeye/providers/market.ts +227 -0
- package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
- package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
- package/src/analytics/birdeye/providers/trending.ts +365 -0
- package/src/analytics/birdeye/providers/wallet.ts +14 -0
- package/src/analytics/birdeye/search-category.test.ts +207 -0
- package/src/analytics/birdeye/search-category.ts +506 -0
- package/src/analytics/birdeye/service.ts +992 -0
- package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
- package/src/analytics/birdeye/types/api/common.ts +305 -0
- package/src/analytics/birdeye/types/api/defi.ts +220 -0
- package/src/analytics/birdeye/types/api/pair.ts +200 -0
- package/src/analytics/birdeye/types/api/search.ts +86 -0
- package/src/analytics/birdeye/types/api/token.ts +635 -0
- package/src/analytics/birdeye/types/api/trader.ts +76 -0
- package/src/analytics/birdeye/types/api/wallet.ts +181 -0
- package/src/analytics/birdeye/types/shared.ts +106 -0
- package/src/analytics/birdeye/utils.ts +700 -0
- package/src/analytics/dexscreener/errors.ts +28 -0
- package/src/analytics/dexscreener/index.ts +3 -0
- package/src/analytics/dexscreener/search-category.test.ts +49 -0
- package/src/analytics/dexscreener/search-category.ts +42 -0
- package/src/analytics/dexscreener/service.ts +595 -0
- package/src/analytics/dexscreener/types.ts +128 -0
- package/src/analytics/lpinfo/index.d.ts +7 -0
- package/src/analytics/lpinfo/index.ts +52 -0
- package/src/analytics/lpinfo/kamino/README.md +102 -0
- package/src/analytics/lpinfo/kamino/index.ts +24 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
- package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
- package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
- package/src/analytics/lpinfo/steer/README.md +169 -0
- package/src/analytics/lpinfo/steer/index.ts +23 -0
- package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
- package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
- package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
- package/src/analytics/news/index.ts +52 -0
- package/src/analytics/news/interfaces/types.ts +222 -0
- package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
- package/src/analytics/news/services/newsDataService.ts +332 -0
- package/src/analytics/news/utils/formatters.ts +151 -0
- package/src/analytics/token-info/action.ts +240 -0
- package/src/analytics/token-info/index.ts +3 -0
- package/src/analytics/token-info/params.ts +215 -0
- package/src/analytics/token-info/providers.ts +681 -0
- package/src/analytics/token-info/service.ts +168 -0
- package/src/analytics/token-info/types.ts +74 -0
- package/src/audit/audit-log.ts +45 -0
- package/src/browser-shim/build-shim.ts +123 -0
- package/src/browser-shim/index.ts +5 -0
- package/src/browser-shim/shim.template.js +563 -0
- package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
- package/src/chains/evm/LICENSE +21 -0
- package/src/chains/evm/README.md +106 -0
- package/src/chains/evm/actions/helpers.ts +147 -0
- package/src/chains/evm/actions/swap.ts +839 -0
- package/src/chains/evm/actions/transfer.ts +254 -0
- package/src/chains/evm/biome.json +61 -0
- package/src/chains/evm/bridge-router.ts +660 -0
- package/src/chains/evm/build.ts +89 -0
- package/src/chains/evm/chain-handler.ts +416 -0
- package/src/chains/evm/constants.ts +23 -0
- package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
- package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
- package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
- package/src/chains/evm/dex/aerodrome/index.ts +34 -0
- package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
- package/src/chains/evm/dex/aerodrome/types.ts +318 -0
- package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
- package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
- package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
- package/src/chains/evm/dex/uniswap/index.ts +35 -0
- package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
- package/src/chains/evm/dex/uniswap/types.ts +390 -0
- package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
- package/src/chains/evm/generated/specs/specs.ts +151 -0
- package/src/chains/evm/gov-router.ts +250 -0
- package/src/chains/evm/index.browser.ts +16 -0
- package/src/chains/evm/index.ts +31 -0
- package/src/chains/evm/prompts.ts +193 -0
- package/src/chains/evm/providers/get-balance.ts +123 -0
- package/src/chains/evm/providers/wallet.ts +715 -0
- package/src/chains/evm/routes/sign.ts +333 -0
- package/src/chains/evm/rpc-providers.ts +410 -0
- package/src/chains/evm/service.ts +140 -0
- package/src/chains/evm/templates/index.ts +10 -0
- package/src/chains/evm/types/index.ts +432 -0
- package/src/chains/evm/vitest.config.ts +18 -0
- package/src/chains/registry.ts +668 -0
- package/src/chains/solana/README.md +367 -0
- package/src/chains/wallet-action.ts +533 -0
- package/src/chains/wallet-router.test.ts +296 -0
- package/src/contracts.ts +65 -0
- package/src/core-augmentation.ts +10 -0
- package/src/index.ts +71 -0
- package/src/lib/server-wallet-trade.ts +192 -0
- package/src/lib/wallet-export-guard.ts +330 -0
- package/src/lp/actions/liquidity.ts +827 -0
- package/src/lp/e2e/real-token-tests.ts +428 -0
- package/src/lp/e2e/scenarios.ts +470 -0
- package/src/lp/e2e/test-utils.ts +145 -0
- package/src/lp/lp-manager-entry.ts +303 -0
- package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
- package/src/lp/services/DexInteractionService.ts +226 -0
- package/src/lp/services/LpManagementService.test.ts +148 -0
- package/src/lp/services/LpManagementService.ts +632 -0
- package/src/lp/services/UserLpProfileService.ts +163 -0
- package/src/lp/services/VaultService.ts +153 -0
- package/src/lp/services/YieldOptimizationService.ts +344 -0
- package/src/lp/services/__tests__/MockLpService.ts +146 -0
- package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
- package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
- package/src/lp/types.ts +582 -0
- package/src/lp/utils/solanaClient.ts +143 -0
- package/src/plugin.ts +125 -0
- package/src/policy/policy.ts +19 -0
- package/src/providers/canonical-provider.ts +27 -0
- package/src/providers/unified-wallet-provider.ts +79 -0
- package/src/register-routes.ts +11 -0
- package/src/routes/plugin.ts +47 -0
- package/src/routes/wallet-market-overview-route.ts +869 -0
- package/src/sdk/abi.ts +258 -0
- package/src/sdk/bridge/abis.ts +126 -0
- package/src/sdk/bridge/client.ts +518 -0
- package/src/sdk/bridge/index.ts +56 -0
- package/src/sdk/bridge/solana.ts +604 -0
- package/src/sdk/bridge/types.ts +202 -0
- package/src/sdk/convenience.ts +347 -0
- package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
- package/src/sdk/escrow/types.ts +64 -0
- package/src/sdk/escrow/verifiers.ts +73 -0
- package/src/sdk/identity/erc8004.ts +692 -0
- package/src/sdk/identity/reputation.ts +449 -0
- package/src/sdk/identity/uaid.ts +497 -0
- package/src/sdk/identity/validation.ts +372 -0
- package/src/sdk/index.ts +763 -0
- package/src/sdk/policy/SpendingPolicy.ts +260 -0
- package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
- package/src/sdk/router/PaymentRouter.ts +215 -0
- package/src/sdk/router/index.ts +8 -0
- package/src/sdk/swap/SwapModule.ts +310 -0
- package/src/sdk/swap/abi.ts +117 -0
- package/src/sdk/swap/index.ts +34 -0
- package/src/sdk/swap/types.ts +135 -0
- package/src/sdk/tokens/decimals.ts +140 -0
- package/src/sdk/tokens/registry.ts +911 -0
- package/src/sdk/tokens/solana.ts +419 -0
- package/src/sdk/tokens/transfers.ts +327 -0
- package/src/sdk/types.ts +158 -0
- package/src/sdk/wallet-core.ts +115 -0
- package/src/sdk/x402/budget.ts +168 -0
- package/src/sdk/x402/chains/abstract/index.ts +280 -0
- package/src/sdk/x402/client.ts +320 -0
- package/src/sdk/x402/index.ts +46 -0
- package/src/sdk/x402/middleware.ts +92 -0
- package/src/sdk/x402/multi-asset.ts +144 -0
- package/src/sdk/x402/types.ts +156 -0
- package/src/services/wallet-backend-service.ts +328 -0
- package/src/types/wallet-router.ts +227 -0
- package/src/utils/intent-trajectory.ts +106 -0
- package/src/wallet/backend.ts +62 -0
- package/src/wallet/errors.ts +49 -0
- package/src/wallet/index.ts +27 -0
- package/src/wallet/local-eoa-backend.ts +201 -0
- package/src/wallet/pending.ts +60 -0
- package/src/wallet/select-backend.ts +47 -0
- package/src/wallet/steward-backend.ts +161 -0
- package/src/wallet-action.ts +1 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import type { ActionResult, HandlerCallback, IAgentRuntime, Memory, State } from "@elizaos/core";
|
|
2
|
+
import { requireActionSpec } from "../generated/specs/spec-helpers";
|
|
3
|
+
import {
|
|
4
|
+
buildSendTxParams,
|
|
5
|
+
confirmationRequired,
|
|
6
|
+
createEvmActionValidator,
|
|
7
|
+
isConfirmed,
|
|
8
|
+
} from "./helpers";
|
|
9
|
+
|
|
10
|
+
const legacySpec = requireActionSpec("EVM_SWAP");
|
|
11
|
+
const spec = { ...legacySpec, name: "WALLET" };
|
|
12
|
+
|
|
13
|
+
import { composePromptFromState, logger, ModelType, parseJSONObjectFromText } from "@elizaos/core";
|
|
14
|
+
import {
|
|
15
|
+
createConfig,
|
|
16
|
+
type ExtendedChain,
|
|
17
|
+
getRoutes,
|
|
18
|
+
getStepTransaction,
|
|
19
|
+
getToken,
|
|
20
|
+
type Route,
|
|
21
|
+
} from "@lifi/sdk";
|
|
22
|
+
|
|
23
|
+
import { type Address, encodeFunctionData, type Hex, parseAbi, parseUnits } from "viem";
|
|
24
|
+
import { runIntentModel } from "../../../utils/intent-trajectory";
|
|
25
|
+
import {
|
|
26
|
+
BEBOP_CHAIN_MAP,
|
|
27
|
+
DEFAULT_SLIPPAGE_PERCENT,
|
|
28
|
+
GAS_BUFFER_MULTIPLIER,
|
|
29
|
+
GAS_PRICE_MULTIPLIER,
|
|
30
|
+
NATIVE_TOKEN_ADDRESS,
|
|
31
|
+
TX_CONFIRMATION_TIMEOUT_MS,
|
|
32
|
+
} from "../constants";
|
|
33
|
+
import { initWalletProvider, type WalletProvider } from "../providers/wallet";
|
|
34
|
+
import { swapTemplate } from "../templates";
|
|
35
|
+
import {
|
|
36
|
+
type BebopRoute,
|
|
37
|
+
BebopRouteSchema,
|
|
38
|
+
EVMError,
|
|
39
|
+
EVMErrorCode,
|
|
40
|
+
parseSwapParams,
|
|
41
|
+
type SupportedChain,
|
|
42
|
+
type SwapParams,
|
|
43
|
+
type SwapQuote,
|
|
44
|
+
type Transaction,
|
|
45
|
+
} from "../types";
|
|
46
|
+
|
|
47
|
+
export { swapTemplate };
|
|
48
|
+
|
|
49
|
+
export class SwapAction {
|
|
50
|
+
constructor(private readonly walletProvider: WalletProvider) {
|
|
51
|
+
const lifiChains: ExtendedChain[] = [];
|
|
52
|
+
|
|
53
|
+
for (const config of Object.values(this.walletProvider.chains)) {
|
|
54
|
+
const blockExplorerUrls = config.blockExplorers?.default?.url
|
|
55
|
+
? [config.blockExplorers.default.url]
|
|
56
|
+
: [];
|
|
57
|
+
|
|
58
|
+
const lifiChain = {
|
|
59
|
+
id: config.id,
|
|
60
|
+
name: config.name,
|
|
61
|
+
key: config.name.toLowerCase(),
|
|
62
|
+
chainType: "EVM",
|
|
63
|
+
nativeToken: {
|
|
64
|
+
...config.nativeCurrency,
|
|
65
|
+
chainId: config.id,
|
|
66
|
+
address: NATIVE_TOKEN_ADDRESS,
|
|
67
|
+
coinKey: config.nativeCurrency.symbol,
|
|
68
|
+
priceUSD: "0",
|
|
69
|
+
logoURI: "",
|
|
70
|
+
symbol: config.nativeCurrency.symbol,
|
|
71
|
+
decimals: config.nativeCurrency.decimals,
|
|
72
|
+
name: config.nativeCurrency.name,
|
|
73
|
+
},
|
|
74
|
+
rpcUrls: {
|
|
75
|
+
public: { http: [config.rpcUrls.default.http[0]] },
|
|
76
|
+
},
|
|
77
|
+
blockExplorerUrls,
|
|
78
|
+
metamask: {
|
|
79
|
+
chainId: `0x${config.id.toString(16)}`,
|
|
80
|
+
chainName: config.name,
|
|
81
|
+
nativeCurrency: config.nativeCurrency,
|
|
82
|
+
rpcUrls: [config.rpcUrls.default.http[0]],
|
|
83
|
+
blockExplorerUrls,
|
|
84
|
+
},
|
|
85
|
+
coin: config.nativeCurrency.symbol,
|
|
86
|
+
mainnet: true,
|
|
87
|
+
diamondAddress: NATIVE_TOKEN_ADDRESS,
|
|
88
|
+
} as ExtendedChain;
|
|
89
|
+
|
|
90
|
+
lifiChains.push(lifiChain);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
createConfig({
|
|
94
|
+
integrator: "eliza",
|
|
95
|
+
chains: lifiChains,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async resolveTokenAddress(
|
|
100
|
+
tokenSymbolOrAddress: string,
|
|
101
|
+
chainId: number
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
if (tokenSymbolOrAddress.startsWith("0x") && tokenSymbolOrAddress.length === 42) {
|
|
104
|
+
return tokenSymbolOrAddress;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (tokenSymbolOrAddress === NATIVE_TOKEN_ADDRESS) {
|
|
108
|
+
return tokenSymbolOrAddress;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const token = await getToken(chainId, tokenSymbolOrAddress);
|
|
112
|
+
return token.address;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async swap(params: SwapParams): Promise<Transaction> {
|
|
116
|
+
// Validate inputs early to fail fast
|
|
117
|
+
const amount = parseFloat(params.amount);
|
|
118
|
+
if (Number.isNaN(amount) || amount <= 0) {
|
|
119
|
+
throw new EVMError(EVMErrorCode.INVALID_PARAMS, "Amount must be a positive number");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
!params.fromToken.startsWith("0x") ||
|
|
124
|
+
(params.fromToken.length !== 42 && params.fromToken !== NATIVE_TOKEN_ADDRESS)
|
|
125
|
+
) {
|
|
126
|
+
throw new EVMError(
|
|
127
|
+
EVMErrorCode.INVALID_PARAMS,
|
|
128
|
+
`Invalid fromToken address: ${params.fromToken}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
!params.toToken.startsWith("0x") ||
|
|
134
|
+
(params.toToken.length !== 42 && params.toToken !== NATIVE_TOKEN_ADDRESS)
|
|
135
|
+
) {
|
|
136
|
+
throw new EVMError(EVMErrorCode.INVALID_PARAMS, `Invalid toToken address: ${params.toToken}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const walletClient = this.walletProvider.getWalletClient(params.chain);
|
|
140
|
+
const [fromAddress] = await walletClient.getAddresses();
|
|
141
|
+
const chainConfig = this.walletProvider.getChainConfigs(params.chain);
|
|
142
|
+
const chainId = chainConfig.id;
|
|
143
|
+
|
|
144
|
+
const resolvedFromToken = await this.resolveTokenAddress(params.fromToken, chainId);
|
|
145
|
+
const resolvedToToken = await this.resolveTokenAddress(params.toToken, chainId);
|
|
146
|
+
|
|
147
|
+
const resolvedParams: SwapParams = {
|
|
148
|
+
...params,
|
|
149
|
+
fromToken: resolvedFromToken as Address,
|
|
150
|
+
toToken: resolvedToToken as Address,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const slippageLevels = [0.01, 0.015, 0.02];
|
|
154
|
+
let lastError: Error | undefined;
|
|
155
|
+
let attemptCount = 0;
|
|
156
|
+
|
|
157
|
+
for (const slippage of slippageLevels) {
|
|
158
|
+
logger.info(`Attempting swap with ${(slippage * 100).toFixed(1)}% slippage...`);
|
|
159
|
+
|
|
160
|
+
const sortedQuotes = await this.getSortedQuotes(fromAddress, resolvedParams, slippage);
|
|
161
|
+
|
|
162
|
+
for (const quote of sortedQuotes) {
|
|
163
|
+
attemptCount++;
|
|
164
|
+
logger.info(`Trying ${quote.aggregator} (attempt ${attemptCount})...`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
let result: Transaction | undefined;
|
|
168
|
+
|
|
169
|
+
switch (quote.aggregator) {
|
|
170
|
+
case "lifi":
|
|
171
|
+
result = await this.executeLifiQuote(quote);
|
|
172
|
+
break;
|
|
173
|
+
case "bebop":
|
|
174
|
+
result = await this.executeBebopQuote(quote, resolvedParams);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (result) {
|
|
179
|
+
logger.info(`✅ Swap succeeded via ${quote.aggregator}!`);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
184
|
+
logger.warn(`${quote.aggregator} attempt failed: ${lastError.message}`);
|
|
185
|
+
|
|
186
|
+
// If it's a recoverable error, continue to next attempt
|
|
187
|
+
if (this.isRecoverableError(lastError)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Non-recoverable error, throw immediately
|
|
192
|
+
throw lastError;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new EVMError(
|
|
200
|
+
EVMErrorCode.CONTRACT_REVERT,
|
|
201
|
+
`All swap attempts failed after ${attemptCount} tries. ${lastError?.message ?? "Unknown error"}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private isRecoverableError(error: Error): boolean {
|
|
206
|
+
const message = error.message;
|
|
207
|
+
return (
|
|
208
|
+
message.includes("price movement") ||
|
|
209
|
+
message.includes("Return amount is not enough") ||
|
|
210
|
+
message.includes("reverted") ||
|
|
211
|
+
message.includes("MEV frontrunning") ||
|
|
212
|
+
message.includes("TRANSFER_FROM_FAILED")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async getSortedQuotes(
|
|
217
|
+
fromAddress: Address,
|
|
218
|
+
params: SwapParams,
|
|
219
|
+
slippage: number = DEFAULT_SLIPPAGE_PERCENT
|
|
220
|
+
): Promise<SwapQuote[]> {
|
|
221
|
+
const decimalsAbi = parseAbi(["function decimals() view returns (uint8)"]);
|
|
222
|
+
let fromTokenDecimals: number;
|
|
223
|
+
|
|
224
|
+
const chainConfig = this.walletProvider.getChainConfigs(params.chain);
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
params.fromToken.toUpperCase() === chainConfig.nativeCurrency.symbol.toUpperCase() ||
|
|
228
|
+
params.fromToken === NATIVE_TOKEN_ADDRESS
|
|
229
|
+
) {
|
|
230
|
+
fromTokenDecimals = chainConfig.nativeCurrency.decimals;
|
|
231
|
+
} else {
|
|
232
|
+
const publicClient = this.walletProvider.getPublicClient(params.chain);
|
|
233
|
+
const decimals = await publicClient.readContract({
|
|
234
|
+
address: params.fromToken as Address,
|
|
235
|
+
abi: decimalsAbi,
|
|
236
|
+
functionName: "decimals",
|
|
237
|
+
authorizationList: undefined,
|
|
238
|
+
});
|
|
239
|
+
fromTokenDecimals = Number(decimals);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const quotesPromises: Promise<SwapQuote | undefined>[] = [
|
|
243
|
+
this.getLifiQuote(fromAddress, params, fromTokenDecimals, slippage),
|
|
244
|
+
this.getBebopQuote(fromAddress, params, fromTokenDecimals),
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const quotesResults = await Promise.all(quotesPromises);
|
|
248
|
+
const sortedQuotes = quotesResults.filter((quote): quote is SwapQuote => quote !== undefined);
|
|
249
|
+
|
|
250
|
+
sortedQuotes.sort((a, b) => (BigInt(a.minOutputAmount) > BigInt(b.minOutputAmount) ? -1 : 1));
|
|
251
|
+
|
|
252
|
+
if (sortedQuotes.length === 0) {
|
|
253
|
+
throw new EVMError(EVMErrorCode.INVALID_PARAMS, "No routes found");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return sortedQuotes;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async getLifiQuote(
|
|
260
|
+
fromAddress: Address,
|
|
261
|
+
params: SwapParams,
|
|
262
|
+
fromTokenDecimals: number,
|
|
263
|
+
slippage: number = DEFAULT_SLIPPAGE_PERCENT
|
|
264
|
+
): Promise<SwapQuote | undefined> {
|
|
265
|
+
try {
|
|
266
|
+
const routes = await getRoutes({
|
|
267
|
+
fromChainId: this.walletProvider.getChainConfigs(params.chain).id,
|
|
268
|
+
toChainId: this.walletProvider.getChainConfigs(params.chain).id,
|
|
269
|
+
fromTokenAddress: params.fromToken,
|
|
270
|
+
toTokenAddress: params.toToken,
|
|
271
|
+
fromAmount: parseUnits(params.amount, fromTokenDecimals).toString(),
|
|
272
|
+
fromAddress,
|
|
273
|
+
options: {
|
|
274
|
+
slippage,
|
|
275
|
+
order: "RECOMMENDED",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!routes.routes.length) {
|
|
280
|
+
throw new Error("No routes found");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
aggregator: "lifi",
|
|
285
|
+
minOutputAmount: routes.routes[0].steps[0].estimate.toAmountMin,
|
|
286
|
+
swapData: routes.routes[0],
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
logger.error(
|
|
290
|
+
"Error in getLifiQuote:",
|
|
291
|
+
error instanceof Error ? error.message : String(error)
|
|
292
|
+
);
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private async getBebopQuote(
|
|
298
|
+
fromAddress: Address,
|
|
299
|
+
params: SwapParams,
|
|
300
|
+
fromTokenDecimals: number
|
|
301
|
+
): Promise<SwapQuote | undefined> {
|
|
302
|
+
try {
|
|
303
|
+
const chainName = BEBOP_CHAIN_MAP[params.chain] ?? params.chain;
|
|
304
|
+
const url = `https://api.bebop.xyz/router/${chainName}/v1/quote`;
|
|
305
|
+
|
|
306
|
+
const chainConfig = this.walletProvider.getChainConfigs(params.chain);
|
|
307
|
+
const resolvedFromToken = await this.resolveTokenAddress(params.fromToken, chainConfig.id);
|
|
308
|
+
const resolvedToToken = await this.resolveTokenAddress(params.toToken, chainConfig.id);
|
|
309
|
+
|
|
310
|
+
const reqParams = new URLSearchParams({
|
|
311
|
+
sell_tokens: resolvedFromToken,
|
|
312
|
+
buy_tokens: resolvedToToken,
|
|
313
|
+
sell_amounts: parseUnits(params.amount, fromTokenDecimals).toString(),
|
|
314
|
+
taker_address: fromAddress,
|
|
315
|
+
approval_type: "Standard",
|
|
316
|
+
skip_validation: "true",
|
|
317
|
+
gasless: "false",
|
|
318
|
+
source: "eliza",
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const response = await fetch(`${url}?${reqParams.toString()}`, {
|
|
322
|
+
method: "GET",
|
|
323
|
+
headers: { accept: "application/json" },
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
throw new Error(`Bebop API error: ${response.status} ${response.statusText}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const data = await response.json();
|
|
331
|
+
|
|
332
|
+
if (!data.routes?.length) {
|
|
333
|
+
throw new Error("No routes found in Bebop API response");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const firstRoute = data.routes[0];
|
|
337
|
+
const quoteTx = firstRoute?.quote?.tx;
|
|
338
|
+
|
|
339
|
+
if (!quoteTx) {
|
|
340
|
+
throw new Error("Invalid route structure in Bebop API response");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const route: BebopRoute = {
|
|
344
|
+
data: quoteTx.data,
|
|
345
|
+
sellAmount: parseUnits(params.amount, fromTokenDecimals).toString(),
|
|
346
|
+
approvalTarget: firstRoute.quote.approvalTarget as Address,
|
|
347
|
+
from: quoteTx.from as Address,
|
|
348
|
+
value: quoteTx.value?.toString() ?? "0",
|
|
349
|
+
to: quoteTx.to as Address,
|
|
350
|
+
gas: quoteTx.gas?.toString() ?? "0",
|
|
351
|
+
gasPrice: quoteTx.gasPrice?.toString() ?? "0",
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Validate the route structure
|
|
355
|
+
BebopRouteSchema.parse(route);
|
|
356
|
+
|
|
357
|
+
// Find buy token info
|
|
358
|
+
const buyTokens = firstRoute.quote.buyTokens;
|
|
359
|
+
if (!buyTokens) {
|
|
360
|
+
throw new Error("Missing buyTokens in Bebop response");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const buyTokenInfo =
|
|
364
|
+
buyTokens[resolvedToToken] ??
|
|
365
|
+
buyTokens[params.toToken] ??
|
|
366
|
+
buyTokens[resolvedToToken.toLowerCase()] ??
|
|
367
|
+
Object.values(buyTokens)[0];
|
|
368
|
+
|
|
369
|
+
if (!buyTokenInfo?.minimumAmount) {
|
|
370
|
+
throw new Error("Cannot determine minimum output amount");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
aggregator: "bebop",
|
|
375
|
+
minOutputAmount: buyTokenInfo.minimumAmount.toString(),
|
|
376
|
+
swapData: route,
|
|
377
|
+
};
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.error(
|
|
380
|
+
"Error in getBebopQuote:",
|
|
381
|
+
error instanceof Error ? error.message : String(error)
|
|
382
|
+
);
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async executeLifiQuote(quote: SwapQuote): Promise<Transaction | undefined> {
|
|
388
|
+
const route = quote.swapData as Route;
|
|
389
|
+
const step = route.steps[0];
|
|
390
|
+
|
|
391
|
+
if (!step) {
|
|
392
|
+
throw new EVMError(EVMErrorCode.INVALID_PARAMS, "No steps found in route");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const stepWithTx = await getStepTransaction(step);
|
|
396
|
+
|
|
397
|
+
if (!stepWithTx.transactionRequest) {
|
|
398
|
+
throw new EVMError(EVMErrorCode.INVALID_PARAMS, "No transaction request found in step");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const chainId = route.fromChainId;
|
|
402
|
+
const chainName = Object.keys(this.walletProvider.chains).find(
|
|
403
|
+
(name) => this.walletProvider.getChainConfigs(name as SupportedChain).id === chainId
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (!chainName) {
|
|
407
|
+
throw new EVMError(EVMErrorCode.CHAIN_NOT_CONFIGURED, `Chain with ID ${chainId} not found`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const walletClient = this.walletProvider.getWalletClient(chainName as SupportedChain);
|
|
411
|
+
const publicClient = this.walletProvider.getPublicClient(chainName as SupportedChain);
|
|
412
|
+
|
|
413
|
+
const account = walletClient.account;
|
|
414
|
+
if (!account) {
|
|
415
|
+
throw new EVMError(EVMErrorCode.WALLET_NOT_INITIALIZED, "Wallet account is not available");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const chain = walletClient.chain;
|
|
419
|
+
if (!chain) {
|
|
420
|
+
throw new EVMError(EVMErrorCode.CHAIN_NOT_CONFIGURED, "Wallet chain is not configured");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const txRequest = stepWithTx.transactionRequest;
|
|
424
|
+
const fromToken = route.fromToken;
|
|
425
|
+
if (fromToken.address !== NATIVE_TOKEN_ADDRESS) {
|
|
426
|
+
await this.handleTokenApproval(
|
|
427
|
+
publicClient,
|
|
428
|
+
walletClient,
|
|
429
|
+
fromToken.address as Address,
|
|
430
|
+
txRequest.to as Address,
|
|
431
|
+
BigInt(route.fromAmount)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const hash = await walletClient.sendTransaction(
|
|
436
|
+
buildSendTxParams({
|
|
437
|
+
account,
|
|
438
|
+
to: txRequest.to as Address,
|
|
439
|
+
value: BigInt(txRequest.value ?? "0"),
|
|
440
|
+
data: txRequest.data as Hex,
|
|
441
|
+
chain,
|
|
442
|
+
gas: txRequest.gasLimit
|
|
443
|
+
? BigInt(Math.floor(Number(txRequest.gasLimit) * GAS_BUFFER_MULTIPLIER))
|
|
444
|
+
: undefined,
|
|
445
|
+
gasPrice: txRequest.gasPrice
|
|
446
|
+
? BigInt(Math.floor(Number(txRequest.gasPrice) * GAS_PRICE_MULTIPLIER))
|
|
447
|
+
: undefined,
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
452
|
+
hash,
|
|
453
|
+
timeout: TX_CONFIRMATION_TIMEOUT_MS,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (receipt.status === "reverted") {
|
|
457
|
+
throw new EVMError(EVMErrorCode.CONTRACT_REVERT, `Transaction reverted. Hash: ${hash}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
hash,
|
|
462
|
+
from: account.address,
|
|
463
|
+
to: txRequest.to as Address,
|
|
464
|
+
value: BigInt(txRequest.value ?? "0"),
|
|
465
|
+
data: txRequest.data as Hex,
|
|
466
|
+
chainId: route.fromChainId,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private async executeBebopQuote(
|
|
471
|
+
quote: SwapQuote,
|
|
472
|
+
params: SwapParams
|
|
473
|
+
): Promise<Transaction | undefined> {
|
|
474
|
+
const bebopRoute = quote.swapData as BebopRoute;
|
|
475
|
+
const walletClient = this.walletProvider.getWalletClient(params.chain);
|
|
476
|
+
const publicClient = this.walletProvider.getPublicClient(params.chain);
|
|
477
|
+
|
|
478
|
+
const account = walletClient.account;
|
|
479
|
+
if (!account) {
|
|
480
|
+
throw new EVMError(EVMErrorCode.WALLET_NOT_INITIALIZED, "Wallet account is not available");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const chainConfig = this.walletProvider.getChainConfigs(params.chain);
|
|
484
|
+
const resolvedFromToken = await this.resolveTokenAddress(params.fromToken, chainConfig.id);
|
|
485
|
+
|
|
486
|
+
if (resolvedFromToken !== NATIVE_TOKEN_ADDRESS) {
|
|
487
|
+
await this.handleTokenApproval(
|
|
488
|
+
publicClient,
|
|
489
|
+
walletClient,
|
|
490
|
+
resolvedFromToken as Address,
|
|
491
|
+
bebopRoute.approvalTarget,
|
|
492
|
+
BigInt(bebopRoute.sellAmount)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const hash = await walletClient.sendTransaction(
|
|
497
|
+
buildSendTxParams({
|
|
498
|
+
account,
|
|
499
|
+
to: bebopRoute.to as Address,
|
|
500
|
+
value: BigInt(bebopRoute.value),
|
|
501
|
+
data: bebopRoute.data as Hex,
|
|
502
|
+
chain: walletClient.chain,
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
507
|
+
hash,
|
|
508
|
+
timeout: TX_CONFIRMATION_TIMEOUT_MS,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (receipt.status === "reverted") {
|
|
512
|
+
throw new EVMError(EVMErrorCode.CONTRACT_REVERT, `Bebop swap reverted. Hash: ${hash}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
hash,
|
|
517
|
+
from: account.address,
|
|
518
|
+
to: bebopRoute.to,
|
|
519
|
+
value: BigInt(bebopRoute.value),
|
|
520
|
+
data: bebopRoute.data as Hex,
|
|
521
|
+
chainId: chainConfig.id,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private async handleTokenApproval(
|
|
526
|
+
publicClient: ReturnType<WalletProvider["getPublicClient"]>,
|
|
527
|
+
walletClient: ReturnType<WalletProvider["getWalletClient"]>,
|
|
528
|
+
tokenAddress: Address,
|
|
529
|
+
spenderAddress: Address,
|
|
530
|
+
requiredAmount: bigint
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
const account = walletClient.account;
|
|
533
|
+
if (!account) {
|
|
534
|
+
throw new EVMError(EVMErrorCode.WALLET_NOT_INITIALIZED, "Wallet account not available");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const allowanceAbi = parseAbi(["function allowance(address,address) view returns (uint256)"]);
|
|
538
|
+
|
|
539
|
+
const allowance = BigInt(
|
|
540
|
+
await publicClient.readContract({
|
|
541
|
+
address: tokenAddress,
|
|
542
|
+
abi: allowanceAbi,
|
|
543
|
+
functionName: "allowance",
|
|
544
|
+
args: [account.address, spenderAddress],
|
|
545
|
+
authorizationList: undefined,
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (allowance >= requiredAmount) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
logger.info(`Approving token for swap...`);
|
|
554
|
+
|
|
555
|
+
const approvalData = encodeFunctionData({
|
|
556
|
+
abi: parseAbi(["function approve(address,uint256)"]),
|
|
557
|
+
functionName: "approve",
|
|
558
|
+
args: [spenderAddress, requiredAmount],
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const approvalTx = await walletClient.sendTransaction(
|
|
562
|
+
buildSendTxParams({
|
|
563
|
+
account,
|
|
564
|
+
to: tokenAddress,
|
|
565
|
+
value: 0n,
|
|
566
|
+
data: approvalData,
|
|
567
|
+
chain: walletClient.chain,
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
logger.info(`Waiting for approval confirmation...`);
|
|
572
|
+
|
|
573
|
+
const approvalReceipt = await publicClient.waitForTransactionReceipt({
|
|
574
|
+
hash: approvalTx,
|
|
575
|
+
timeout: TX_CONFIRMATION_TIMEOUT_MS,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
if (approvalReceipt.status === "reverted") {
|
|
579
|
+
throw new EVMError(
|
|
580
|
+
EVMErrorCode.CONTRACT_REVERT,
|
|
581
|
+
`Token approval failed. Hash: ${approvalTx}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
logger.info(`Token approval confirmed`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function buildSwapDetails(
|
|
590
|
+
state: State,
|
|
591
|
+
message: Memory,
|
|
592
|
+
runtime: IAgentRuntime,
|
|
593
|
+
wp: WalletProvider
|
|
594
|
+
): Promise<SwapParams> {
|
|
595
|
+
const chains = wp.getSupportedChains();
|
|
596
|
+
const balances = await wp.getWalletBalances();
|
|
597
|
+
|
|
598
|
+
state = await runtime.composeState(message, ["RECENT_MESSAGES"], true);
|
|
599
|
+
state.supportedChains = chains.join(" | ");
|
|
600
|
+
state.chainBalances = Object.entries(balances)
|
|
601
|
+
.map(([chain, balance]) => {
|
|
602
|
+
const chainConfig = wp.getChainConfigs(chain as SupportedChain);
|
|
603
|
+
return `${chain}: ${balance} ${chainConfig.nativeCurrency.symbol}`;
|
|
604
|
+
})
|
|
605
|
+
.join(", ");
|
|
606
|
+
|
|
607
|
+
const context = composePromptFromState({
|
|
608
|
+
state,
|
|
609
|
+
template: swapTemplate,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const llmResponse = await runIntentModel({
|
|
613
|
+
runtime,
|
|
614
|
+
taskName: "evm.swap.intent",
|
|
615
|
+
template: context,
|
|
616
|
+
modelType: ModelType.TEXT_LARGE,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const parsedResponse = parseJSONObjectFromText(llmResponse) as Record<string, unknown> | null;
|
|
620
|
+
|
|
621
|
+
if (!parsedResponse) {
|
|
622
|
+
throw new EVMError(
|
|
623
|
+
EVMErrorCode.INVALID_PARAMS,
|
|
624
|
+
"Failed to parse structured response from LLM for swap details."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const rawParams = {
|
|
629
|
+
fromToken: String(parsedResponse.inputToken ?? ""),
|
|
630
|
+
toToken: String(parsedResponse.outputToken ?? ""),
|
|
631
|
+
amount: String(parsedResponse.amount ?? ""),
|
|
632
|
+
chain: String(parsedResponse.chain ?? "").toLowerCase(),
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const swapDetails = parseSwapParams(rawParams);
|
|
636
|
+
|
|
637
|
+
if (!wp.chains[swapDetails.chain]) {
|
|
638
|
+
throw new EVMError(
|
|
639
|
+
EVMErrorCode.CHAIN_NOT_CONFIGURED,
|
|
640
|
+
`Chain ${swapDetails.chain} not configured. Available: ${chains.join(", ")}`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const messageText = (message.content.text ?? "").toLowerCase();
|
|
645
|
+
if (swapDetails.amount === "null") {
|
|
646
|
+
const balance = balances[swapDetails.chain];
|
|
647
|
+
if (balance) {
|
|
648
|
+
if (messageText.includes("half") || messageText.includes("50%")) {
|
|
649
|
+
return { ...swapDetails, amount: (parseFloat(balance) / 2).toString() };
|
|
650
|
+
}
|
|
651
|
+
if (
|
|
652
|
+
messageText.includes("all") ||
|
|
653
|
+
messageText.includes("100%") ||
|
|
654
|
+
messageText.includes("everything")
|
|
655
|
+
) {
|
|
656
|
+
return {
|
|
657
|
+
...swapDetails,
|
|
658
|
+
amount: (parseFloat(balance) * 0.9).toString(),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const percentMatch = messageText.match(/(\d+)%/);
|
|
662
|
+
if (percentMatch) {
|
|
663
|
+
const percentage = parseInt(percentMatch[1], 10) / 100;
|
|
664
|
+
return {
|
|
665
|
+
...swapDetails,
|
|
666
|
+
amount: (parseFloat(balance) * percentage).toString(),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return swapDetails;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export const swapAction = {
|
|
676
|
+
name: spec.name,
|
|
677
|
+
description: spec.description,
|
|
678
|
+
descriptionCompressed: spec.descriptionCompressed,
|
|
679
|
+
contexts: ["finance", "crypto", "wallet"],
|
|
680
|
+
contextGate: { anyOf: ["finance", "crypto", "wallet"] },
|
|
681
|
+
roleGate: { minRole: "USER" },
|
|
682
|
+
parameters: [
|
|
683
|
+
{
|
|
684
|
+
name: "fromToken",
|
|
685
|
+
description: "Input token symbol or address.",
|
|
686
|
+
required: true,
|
|
687
|
+
schema: { type: "string" },
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: "toToken",
|
|
691
|
+
description: "Output token symbol or address.",
|
|
692
|
+
required: true,
|
|
693
|
+
schema: { type: "string" },
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: "amount",
|
|
697
|
+
description: "Human-readable amount to swap.",
|
|
698
|
+
required: true,
|
|
699
|
+
schema: { type: "string" },
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "chain",
|
|
703
|
+
description: "EVM chain for the swap.",
|
|
704
|
+
required: false,
|
|
705
|
+
schema: { type: "string" },
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: "confirmed",
|
|
709
|
+
description: "Set true after preview confirmation to submit.",
|
|
710
|
+
required: false,
|
|
711
|
+
schema: { type: "boolean", default: false },
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
|
|
715
|
+
handler: async (
|
|
716
|
+
runtime: IAgentRuntime,
|
|
717
|
+
message: Memory,
|
|
718
|
+
state?: State,
|
|
719
|
+
options?: Record<string, unknown>,
|
|
720
|
+
callback?: HandlerCallback
|
|
721
|
+
): Promise<ActionResult> => {
|
|
722
|
+
const walletProvider = await initWalletProvider(runtime);
|
|
723
|
+
|
|
724
|
+
if (!state) {
|
|
725
|
+
state = await runtime.composeState(message);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const swapOptions = await buildSwapDetails(state, message, runtime, walletProvider);
|
|
729
|
+
|
|
730
|
+
if (!isConfirmed(options)) {
|
|
731
|
+
const preview = `Review EVM swap before submitting: ${swapOptions.amount} ${swapOptions.fromToken} to ${swapOptions.toToken} on ${swapOptions.chain}. Re-invoke ${spec.name} with confirmed: true to submit.`;
|
|
732
|
+
return confirmationRequired({
|
|
733
|
+
actionName: spec.name,
|
|
734
|
+
preview,
|
|
735
|
+
parameters: swapOptions,
|
|
736
|
+
callback,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const action = new SwapAction(walletProvider);
|
|
741
|
+
const swapResp = await action.swap(swapOptions);
|
|
742
|
+
|
|
743
|
+
const successText = `✅ Successfully swapped ${swapOptions.amount} ${swapOptions.fromToken} for ${swapOptions.toToken} on ${swapOptions.chain}\nTransaction Hash: ${swapResp.hash}`;
|
|
744
|
+
|
|
745
|
+
if (callback) {
|
|
746
|
+
callback({
|
|
747
|
+
text: successText,
|
|
748
|
+
content: {
|
|
749
|
+
success: true,
|
|
750
|
+
hash: swapResp.hash,
|
|
751
|
+
chain: swapOptions.chain,
|
|
752
|
+
fromToken: swapOptions.fromToken,
|
|
753
|
+
toToken: swapOptions.toToken,
|
|
754
|
+
amount: swapOptions.amount,
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
success: true,
|
|
761
|
+
text: successText,
|
|
762
|
+
values: {
|
|
763
|
+
swapSucceeded: true,
|
|
764
|
+
inputToken: swapOptions.fromToken,
|
|
765
|
+
outputToken: swapOptions.toToken,
|
|
766
|
+
},
|
|
767
|
+
data: {
|
|
768
|
+
actionName: "EVM_SWAP_TOKENS",
|
|
769
|
+
transactionHash: swapResp.hash,
|
|
770
|
+
chain: swapOptions.chain,
|
|
771
|
+
fromToken: swapOptions.fromToken,
|
|
772
|
+
toToken: swapOptions.toToken,
|
|
773
|
+
amount: swapOptions.amount,
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
template: swapTemplate,
|
|
779
|
+
|
|
780
|
+
validate: createEvmActionValidator({
|
|
781
|
+
keywords: ["swap", "exchange", "trade", "token"],
|
|
782
|
+
regex: /\b(?:swap|exchange|trade|token)\b/i,
|
|
783
|
+
}),
|
|
784
|
+
|
|
785
|
+
examples: [
|
|
786
|
+
[
|
|
787
|
+
{
|
|
788
|
+
name: "user",
|
|
789
|
+
user: "user",
|
|
790
|
+
content: {
|
|
791
|
+
text: "Swap 1 WETH for USDC on Arbitrum",
|
|
792
|
+
action: "TOKEN_SWAP",
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
[
|
|
797
|
+
{
|
|
798
|
+
name: "user",
|
|
799
|
+
user: "user",
|
|
800
|
+
content: {
|
|
801
|
+
text: "Please exchange 250 USDC to ETH on Base",
|
|
802
|
+
action: "TOKEN_SWAP",
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
[
|
|
807
|
+
{
|
|
808
|
+
name: "user",
|
|
809
|
+
user: "user",
|
|
810
|
+
content: {
|
|
811
|
+
text: "Intercambia la mitad de mis USDT por ETH en Arbitrum",
|
|
812
|
+
action: "TOKEN_SWAP",
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
[
|
|
817
|
+
{
|
|
818
|
+
name: "user",
|
|
819
|
+
user: "user",
|
|
820
|
+
content: {
|
|
821
|
+
text: "把我全部的 USDC 换成 WETH,在 Base 上",
|
|
822
|
+
action: "TOKEN_SWAP",
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
[
|
|
827
|
+
{
|
|
828
|
+
name: "user",
|
|
829
|
+
user: "user",
|
|
830
|
+
content: {
|
|
831
|
+
text: "Trade 30% of my ETH balance into USDC",
|
|
832
|
+
action: "TOKEN_SWAP",
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
],
|
|
837
|
+
|
|
838
|
+
similes: spec.similes ? [...spec.similes] : [],
|
|
839
|
+
};
|