Skip to content

Commit 9cffcbe

Browse files
authored
Merge pull request #18 from elizaos-plugins/fix/transfer-erc-20
fix: erc20 transfer
2 parents e24f634 + 2e29493 commit 9cffcbe

3 files changed

Lines changed: 130 additions & 19 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@elizaos/plugin-evm",
3-
"version": "1.0.10",
3+
"version": "1.0.11",
44
"type": "module",
55
"main": "dist/index.js",
66
"module": "dist/index.js",

src/actions/transfer.ts

Lines changed: 128 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@ import {
77
type State,
88
parseKeyValueXml,
99
composePromptFromState,
10+
elizaLogger,
1011
} from '@elizaos/core';
11-
import { type Hex, formatEther, parseEther } from 'viem';
12+
import {
13+
type Hex,
14+
formatEther,
15+
parseEther,
16+
parseAbi,
17+
encodeFunctionData,
18+
parseUnits,
19+
type Address,
20+
} from 'viem';
21+
import { getToken } from '@lifi/sdk';
1222

1323
import { type WalletProvider, initWalletProvider } from '../providers/wallet';
1424
import { transferTemplate } from '../templates';
@@ -19,37 +29,125 @@ export class TransferAction {
1929
constructor(private walletProvider: WalletProvider) {}
2030

2131
async transfer(params: TransferParams): Promise<Transaction> {
22-
if (!params.data) {
23-
params.data = '0x';
24-
}
25-
2632
const walletClient = this.walletProvider.getWalletClient(params.fromChain);
2733

2834
if (!walletClient.account) {
2935
throw new Error('Wallet account is not available');
3036
}
3137

38+
const chainConfig = this.walletProvider.getChainConfigs(params.fromChain);
39+
3240
try {
33-
const hash = await walletClient.sendTransaction({
41+
let hash: Hex;
42+
let to: Address;
43+
let value: bigint;
44+
let data: Hex;
45+
46+
// Check if this is a token transfer or native transfer
47+
if (
48+
params.token &&
49+
params.token !== 'null' &&
50+
params.token.toUpperCase() !== chainConfig.nativeCurrency.symbol.toUpperCase()
51+
) {
52+
// This is an ERC20 token transfer
53+
console.log(
54+
`Processing ${params.token} token transfer of ${params.amount} to ${params.toAddress}`
55+
);
56+
57+
// First, resolve the token address
58+
const tokenAddress = await this.resolveTokenAddress(params.token, chainConfig.id);
59+
60+
// Check if token was resolved properly
61+
if (tokenAddress === params.token && !tokenAddress.startsWith('0x')) {
62+
throw new Error(
63+
`Token ${params.token} not found on ${params.fromChain}. Please check the token symbol.`
64+
);
65+
}
66+
67+
// Get token decimals
68+
const decimalsAbi = parseAbi(['function decimals() view returns (uint8)']);
69+
const decimals = await this.walletProvider.getPublicClient(params.fromChain).readContract({
70+
address: tokenAddress as Address,
71+
abi: decimalsAbi,
72+
functionName: 'decimals',
73+
});
74+
75+
// Parse amount with correct decimals
76+
const amountInTokenUnits = parseUnits(params.amount, decimals);
77+
78+
// Encode the ERC20 transfer function
79+
const transferData = encodeFunctionData({
80+
abi: parseAbi(['function transfer(address to, uint256 amount)']),
81+
functionName: 'transfer',
82+
args: [params.toAddress, amountInTokenUnits],
83+
});
84+
85+
// For token transfers, we send to the token contract with 0 ETH value
86+
to = tokenAddress as Address;
87+
value = 0n;
88+
data = transferData;
89+
} else {
90+
// This is a native ETH transfer
91+
console.log(
92+
`Processing native ${chainConfig.nativeCurrency.symbol} transfer of ${params.amount} to ${params.toAddress}`
93+
);
94+
95+
to = params.toAddress;
96+
value = parseEther(params.amount);
97+
data = params.data || ('0x' as Hex);
98+
}
99+
100+
const transactionParams = {
34101
account: walletClient.account,
35-
to: params.toAddress,
36-
value: parseEther(params.amount),
37-
data: params.data as Hex,
102+
to,
103+
value,
104+
data,
38105
chain: walletClient.chain,
39-
});
106+
};
107+
108+
hash = await walletClient.sendTransaction(transactionParams);
109+
console.log(`Transaction sent successfully. Hash: ${hash}`);
40110

41111
return {
42112
hash,
43113
from: walletClient.account.address,
44-
to: params.toAddress,
45-
value: parseEther(params.amount),
46-
data: params.data as Hex,
114+
to: params.toAddress, // Always return the recipient address, not the contract
115+
value: value,
116+
data: data,
47117
};
48118
} catch (error: unknown) {
49119
const errorMessage = error instanceof Error ? error.message : String(error);
50120
throw new Error(`Transfer failed: ${errorMessage}`);
51121
}
52122
}
123+
124+
private async resolveTokenAddress(
125+
tokenSymbolOrAddress: string,
126+
chainId: number
127+
): Promise<string> {
128+
// If it's already a valid address (starts with 0x and is 42 chars), return as is
129+
if (tokenSymbolOrAddress.startsWith('0x') && tokenSymbolOrAddress.length === 42) {
130+
return tokenSymbolOrAddress;
131+
}
132+
133+
// If it's the zero address (native token), return as is
134+
if (tokenSymbolOrAddress === '0x0000000000000000000000000000000000000000') {
135+
return tokenSymbolOrAddress;
136+
}
137+
138+
try {
139+
// Use LiFi SDK to resolve token symbol to address
140+
const token = await getToken(chainId, tokenSymbolOrAddress);
141+
return token.address;
142+
} catch (error) {
143+
elizaLogger.error(
144+
`Failed to resolve token ${tokenSymbolOrAddress} on chain ${chainId}:`,
145+
error
146+
);
147+
// If LiFi fails, return original value and let downstream handle the error
148+
return tokenSymbolOrAddress;
149+
}
150+
}
53151
}
54152

55153
const buildTransferDetails = async (
@@ -112,7 +210,8 @@ const buildTransferDetails = async (
112210

113211
export const transferAction: Action = {
114212
name: 'EVM_TRANSFER_TOKENS',
115-
description: 'Transfer tokens between addresses on the same chain',
213+
description:
214+
'Transfer native tokens (ETH, BNB, etc.) or ERC20 tokens (USDC, USDT, etc.) between addresses on the same chain',
116215
handler: async (
117216
runtime: IAgentRuntime,
118217
message: Memory,
@@ -132,13 +231,24 @@ export const transferAction: Action = {
132231

133232
try {
134233
const transferResp = await action.transfer(paramOptions);
234+
235+
// Determine token symbol for display
236+
const chainConfig = walletProvider.getChainConfigs(paramOptions.fromChain);
237+
const tokenSymbol =
238+
paramOptions.token &&
239+
paramOptions.token !== 'null' &&
240+
paramOptions.token.toUpperCase() !== chainConfig.nativeCurrency.symbol.toUpperCase()
241+
? paramOptions.token.toUpperCase()
242+
: chainConfig.nativeCurrency.symbol;
243+
135244
if (callback) {
136245
callback({
137-
text: `Successfully transferred ${paramOptions.amount} tokens to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`,
246+
text: `Successfully transferred ${paramOptions.amount} ${tokenSymbol} to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`,
138247
content: {
139248
success: true,
140249
hash: transferResp.hash,
141-
amount: formatEther(transferResp.value),
250+
amount: paramOptions.amount,
251+
token: tokenSymbol,
142252
recipient: transferResp.to,
143253
chain: paramOptions.fromChain,
144254
},
@@ -167,14 +277,14 @@ export const transferAction: Action = {
167277
name: 'assistant',
168278
content: {
169279
text: "I'll help you transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
170-
action: 'SEND_TOKENS',
280+
action: 'EVM_TRANSFER_TOKENS',
171281
},
172282
},
173283
{
174284
name: 'user',
175285
content: {
176286
text: 'Transfer 1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
177-
action: 'SEND_TOKENS',
287+
action: 'EVM_TRANSFER_TOKENS',
178288
},
179289
},
180290
],

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface TransferParams {
6767
toAddress: Address;
6868
amount: string;
6969
data?: `0x${string}`;
70+
token?: string; // Token symbol or null for native transfers
7071
}
7172

7273
export interface SwapParams {

0 commit comments

Comments
 (0)