Skip to content

Commit 2d23619

Browse files
authored
Add native ETH transfer support to AA demo app (#3671)
1 parent 4c8cd53 commit 2d23619

File tree

6 files changed

+162
-54
lines changed

6 files changed

+162
-54
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,11 @@ jobs:
609609
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
610610
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
611611
612+
- name: Install system dependencies for anchor
613+
run: |
614+
sudo apt-get update
615+
sudo apt-get install -y libudev-dev
616+
612617
- name: Install anchor
613618
run: |
614619
cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked

tee-worker/omni-executor/aa-contracts/aa-demo-app/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A Next.js application demonstrating Account Abstraction (ERC-4337) functionality
77
This application demonstrates:
88
- **Account Abstraction (AA)**: Create smart contract wallets (OmniAccounts) that can be controlled by multiple signers
99
- **Multi-Signer Management**: Add and remove authorized signers to control the smart account
10-
- **ERC20 Token Transfers**: Send USDC and USDT through the TEE worker using UserOperations
10+
- **Token Transfers**: Send ETH, USDC, and USDT through the TEE worker using UserOperations
1111
- **Real Token Support**: Use actual deployed tokens instead of test tokens
1212
- **Non-Custodial Flow**: Users maintain full control of their accounts while enabling delegated operations
1313
- **ERC-4337 Integration**: Implements the ERC-4337 standard for account abstraction
@@ -17,7 +17,7 @@ This application demonstrates:
1717
## Key Features
1818

1919
- **Separated Funding Flow**: Fund with ETH first for gas
20-
- **Multi-Token Support**: Send USDC and USDT transfers through TEE worker
20+
- **Multi-Token Support**: Send ETH, USDC, and USDT transfers through TEE worker
2121
- **Signer Management**: View, add, and remove authorized signers through the UI
2222
- **Token Balance Display**: Monitor all token balances in real-time
2323
- **TEE Worker Integration**: Execute token transfers securely through TEE worker
@@ -235,8 +235,8 @@ pnpm dev
235235
- Enables secure delegated transaction execution
236236
- The worker address will be tagged in the signers list
237237

238-
5. **Send Token Transfer**: Transfer USDC or USDT through the TEE worker
239-
- Select the token you want to transfer (USDC or USDT)
238+
5. **Send Token Transfer**: Transfer ETH, USDC, or USDT through the TEE worker
239+
- Select the token you want to transfer (ETH, USDC, or USDT)
240240
- Enter recipient address and amount
241241
- The transfer is executed through a UserOperation signed by the TEE worker
242242
- Monitor transaction status in real-time
@@ -287,7 +287,7 @@ aa-demo-app/
287287
│ │ ├── AuthorizedSigners.tsx # Manage authorized signers (direct calls, no execute wrapper)
288288
│ │ ├── AuthorizeTEEWorker.tsx # TEE worker authorization flow (uses Owner signer type)
289289
│ │ ├── FundingGuide.tsx # ETH funding guide
290-
│ │ ├── TEETokenTransfer.tsx # Token transfer through TEE worker
290+
│ │ ├── TEETokenTransfer.tsx # ETH and token transfers through TEE worker
291291
│ │ ├── CreateOmniAccount.tsx # Smart account creation flow with paymaster option
292292
│ │ └── WalletConnect.tsx # Wallet connection component
293293
│ ├── contracts/ # Contract ABIs (auto-synced from V1 contracts)
@@ -324,9 +324,9 @@ Parent directory scripts:
324324

325325
### Token Operations
326326
- Make sure USDC and USDT addresses are set in .env.local
327-
- Ensure you have ETH for gas fees before sending token transfers
328-
- Token transfers require TEE worker to be authorized
329-
- Check that your Omni Account has sufficient token balance
327+
- Ensure you have ETH for gas fees before sending any transfers
328+
- All transfers (ETH and tokens) require TEE worker to be authorized
329+
- Check that your Omni Account has sufficient balance for the transfer
330330

331331
### Anvil Errors
332332
You might see errors like `execution reverted` for `symbol()` or `decimals()` calls. These are harmless - they're from wallets trying to detect if addresses are ERC20 tokens.
@@ -452,6 +452,7 @@ The app integrates paymaster functionality for gas sponsorship:
452452
### Key Utility Functions
453453

454454
- `buildTokenTransferUserOp()`: Creates UserOperation for ERC20 token transfers
455+
- `buildNativeTransferUserOp()`: Creates UserOperation for native ETH transfers
455456
- `buildERC20TransferCallData()`: Encodes ERC20 transfer function call
456457
- `toSerializablePackedUserOperation()`: Converts PackedUserOperation to serializable format
457458
- `submitUserOpTest()`: Submits UserOperations through TEE worker RPC

tee-worker/omni-executor/aa-contracts/aa-demo-app/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ function HomeContent() {
497497
Step 5: Send Token Transfer
498498
</h2>
499499
<p className="text-gray-600 mb-6">
500-
Transfer USDC or USDT through the TEE worker using UserOperations.
500+
Transfer ETH, USDC, or USDT through the TEE worker using UserOperations.
501501
</p>
502502
<TEETokenTransfer
503503
omniAccountAddress={omniAccountAddress}

tee-worker/omni-executor/aa-contracts/aa-demo-app/src/components/TEETokenTransfer.tsx

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { ERC20_TOKENS, CONTRACTS, DEFAULT_CLIENT_ID } from "@/lib/constants";
66
import { submitUserOpTest } from "@/lib/tee-worker-client";
77
import {
88
buildTokenTransferUserOp,
9+
buildNativeTransferUserOp,
910
packUserOperation,
1011
toSerializablePackedUserOperation,
11-
estimateUserOperationGas,
1212
estimateUserOpGasFromWorker,
1313
} from "@/lib/aa-utils";
1414

@@ -34,7 +34,7 @@ export function TEETokenTransfer({
3434
}: TEETokenTransferProps) {
3535
const publicClient = usePublicClient();
3636
const chainId = useChainId();
37-
const [selectedToken, setSelectedToken] = useState<"USDC" | "USDT">("USDC");
37+
const [selectedToken, setSelectedToken] = useState<"ETH" | "USDC" | "USDT">("ETH");
3838
const [recipient, setRecipient] = useState("");
3939
const [amount, setAmount] = useState("");
4040
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -43,15 +43,42 @@ export function TEETokenTransfer({
4343
const [tokenBalances, setTokenBalances] = useState<TokenBalance[]>([]);
4444
const [nonce, setNonce] = useState<bigint>(BigInt(0));
4545

46-
// Available tokens
47-
const availableTokens = [ERC20_TOKENS.USDC, ERC20_TOKENS.USDT];
46+
// Available tokens including ETH
47+
const availableTokens = [
48+
{ symbol: "ETH", decimals: 18, address: "0x0000000000000000000000000000000000000000" as `0x${string}`, isNative: true },
49+
ERC20_TOKENS.USDC,
50+
ERC20_TOKENS.USDT
51+
];
4852

4953
// Fetch token balances
5054
const fetchBalances = async () => {
5155
if (!omniAccountAddress || !publicClient) return;
5256

5357
const balances: TokenBalance[] = [];
54-
for (const token of availableTokens) {
58+
59+
// Fetch ETH balance first
60+
try {
61+
const ethBalance = await publicClient.getBalance({
62+
address: omniAccountAddress as `0x${string}`,
63+
});
64+
balances.push({
65+
symbol: "ETH",
66+
balance: ethBalance,
67+
decimals: 18,
68+
address: "0x0000000000000000000000000000000000000000" as `0x${string}`,
69+
});
70+
} catch (error) {
71+
console.error("Error fetching ETH balance:", error);
72+
balances.push({
73+
symbol: "ETH",
74+
balance: BigInt(0),
75+
decimals: 18,
76+
address: "0x0000000000000000000000000000000000000000" as `0x${string}`,
77+
});
78+
}
79+
80+
// Fetch ERC20 balances
81+
for (const token of [ERC20_TOKENS.USDC, ERC20_TOKENS.USDT]) {
5582
try {
5683
const balance = (await publicClient.readContract({
5784
address: token.address,
@@ -122,46 +149,64 @@ export function TEETokenTransfer({
122149
return;
123150
}
124151

125-
const token = selectedToken === "USDC" ? ERC20_TOKENS.USDC : ERC20_TOKENS.USDT;
126152
const tokenBalance = tokenBalances.find(tb => tb.symbol === selectedToken);
127153

128154
if (!tokenBalance) {
129155
setError("Token balance not loaded");
130156
return;
131157
}
132158

133-
const amountBigInt = parseUnits(amount, token.decimals);
159+
const decimals = selectedToken === "ETH" ? 18 :
160+
selectedToken === "USDC" ? ERC20_TOKENS.USDC.decimals :
161+
ERC20_TOKENS.USDT.decimals;
162+
const amountBigInt = parseUnits(amount, decimals);
134163

135164
if (amountBigInt > tokenBalance.balance) {
136-
setError("Insufficient token balance");
165+
setError("Insufficient balance");
137166
return;
138167
}
139168

140169
setIsSubmitting(true);
141170

142171
try {
143-
// Build the initial UserOperation for token transfer with minimal gas for estimation
144-
const userOpWithoutGas = buildTokenTransferUserOp({
145-
omniAccountAddress: omniAccountAddress as `0x${string}`,
146-
tokenAddress: token.address,
147-
recipient: recipient as `0x${string}`,
148-
amount: amountBigInt,
149-
nonce,
150-
forGasEstimation: true, // Use dummy signature for gas estimation
151-
gasParams: {
152-
// Use minimal gas values for estimation to avoid prefund issues
153-
callGasLimit: BigInt(100000), // Minimal for simulation
154-
verificationGasLimit: BigInt(150000), // Enough for signature validation
155-
preVerificationGas: BigInt(21000), // Base transaction cost
156-
maxFeePerGas: BigInt(1000000000), // 1 gwei - minimal for simulation
157-
maxPriorityFeePerGas: BigInt(1000000000), // 1 gwei - minimal
158-
}
159-
});
172+
// Build the initial UserOperation for transfer with minimal gas for estimation
173+
const userOpForEstimation = selectedToken === "ETH" ?
174+
buildNativeTransferUserOp({
175+
omniAccountAddress: omniAccountAddress as `0x${string}`,
176+
recipient: recipient as `0x${string}`,
177+
amount: amountBigInt,
178+
nonce,
179+
forGasEstimation: true, // Use dummy signature for gas estimation
180+
gasParams: {
181+
// Use minimal gas values for estimation to avoid prefund issues
182+
callGasLimit: BigInt(100000), // Minimal for simulation
183+
verificationGasLimit: BigInt(150000), // Enough for signature validation
184+
preVerificationGas: BigInt(21000), // Base transaction cost
185+
maxFeePerGas: BigInt(1000000000), // 1 gwei - minimal for simulation
186+
maxPriorityFeePerGas: BigInt(1000000000), // 1 gwei - minimal
187+
}
188+
}) :
189+
buildTokenTransferUserOp({
190+
omniAccountAddress: omniAccountAddress as `0x${string}`,
191+
tokenAddress: selectedToken === "USDC" ? ERC20_TOKENS.USDC.address : ERC20_TOKENS.USDT.address,
192+
recipient: recipient as `0x${string}`,
193+
amount: amountBigInt,
194+
nonce,
195+
forGasEstimation: true, // Use dummy signature for gas estimation
196+
gasParams: {
197+
// Use minimal gas values for estimation to avoid prefund issues
198+
callGasLimit: BigInt(100000), // Minimal for simulation
199+
verificationGasLimit: BigInt(150000), // Enough for signature validation
200+
preVerificationGas: BigInt(21000), // Base transaction cost
201+
maxFeePerGas: BigInt(1000000000), // 1 gwei - minimal for simulation
202+
maxPriorityFeePerGas: BigInt(1000000000), // 1 gwei - minimal
203+
}
204+
});
160205

161206
// Estimate gas using the TEE worker
162207
console.log("Attempting to estimate gas using TEE worker...");
163208
const gasParams = await estimateUserOpGasFromWorker(
164-
userOpWithoutGas,
209+
userOpForEstimation,
165210
chainId,
166211
0, // wallet_index
167212
omniAccountHash,
@@ -171,14 +216,22 @@ export function TEETokenTransfer({
171216
console.log("Successfully estimated gas using TEE worker:", gasParams);
172217

173218
// Build the final UserOperation with gas estimates
174-
const userOp = buildTokenTransferUserOp({
175-
omniAccountAddress: omniAccountAddress as `0x${string}`,
176-
tokenAddress: token.address,
177-
recipient: recipient as `0x${string}`,
178-
amount: amountBigInt,
179-
nonce,
180-
gasParams,
181-
});
219+
const userOp = selectedToken === "ETH" ?
220+
buildNativeTransferUserOp({
221+
omniAccountAddress: omniAccountAddress as `0x${string}`,
222+
recipient: recipient as `0x${string}`,
223+
amount: amountBigInt,
224+
nonce,
225+
gasParams,
226+
}) :
227+
buildTokenTransferUserOp({
228+
omniAccountAddress: omniAccountAddress as `0x${string}`,
229+
tokenAddress: selectedToken === "USDC" ? ERC20_TOKENS.USDC.address : ERC20_TOKENS.USDT.address,
230+
recipient: recipient as `0x${string}`,
231+
amount: amountBigInt,
232+
nonce,
233+
gasParams,
234+
});
182235

183236
// Pack the UserOperation
184237
const packedOp = packUserOperation(userOp);
@@ -258,7 +311,7 @@ export function TEETokenTransfer({
258311
<Send className="mx-auto h-12 w-12 text-blue-500 mb-4" />
259312
<h2 className="text-2xl font-bold">Send Token Transfer</h2>
260313
<p className="text-gray-600 mt-2">
261-
Transfer USDC or USDT through the TEE worker
314+
Transfer ETH, USDC, or USDT through the TEE worker
262315
</p>
263316
</div>
264317

@@ -270,14 +323,18 @@ export function TEETokenTransfer({
270323
<div
271324
key={tb.symbol}
272325
className={`p-3 rounded-lg flex justify-between items-center cursor-pointer transition-colors ${selectedToken === tb.symbol
273-
? tb.symbol === "USDC"
274-
? "bg-blue-100 border-2 border-blue-500"
275-
: "bg-green-100 border-2 border-green-500"
276-
: tb.symbol === "USDC"
277-
? "bg-blue-50 hover:bg-blue-100"
278-
: "bg-green-50 hover:bg-green-100"
326+
? tb.symbol === "ETH"
327+
? "bg-purple-100 border-2 border-purple-500"
328+
: tb.symbol === "USDC"
329+
? "bg-blue-100 border-2 border-blue-500"
330+
: "bg-green-100 border-2 border-green-500"
331+
: tb.symbol === "ETH"
332+
? "bg-purple-50 hover:bg-purple-100"
333+
: tb.symbol === "USDC"
334+
? "bg-blue-50 hover:bg-blue-100"
335+
: "bg-green-50 hover:bg-green-100"
279336
}`}
280-
onClick={() => setSelectedToken(tb.symbol as "USDC" | "USDT")}
337+
onClick={() => setSelectedToken(tb.symbol as "ETH" | "USDC" | "USDT")}
281338
>
282339
<span className="font-medium">{tb.symbol}</span>
283340
<span className="font-mono text-sm">
@@ -369,9 +426,9 @@ export function TEETokenTransfer({
369426
<div className="text-sm text-blue-700">
370427
<p className="font-medium mb-1">How it works:</p>
371428
<ul className="list-disc list-inside space-y-1">
372-
<li>This creates a UserOperation for an ERC20 transfer</li>
429+
<li>This creates a UserOperation for {selectedToken === "ETH" ? "a native ETH" : "an ERC20"} transfer</li>
373430
<li>The TEE worker signs and submits the operation</li>
374-
<li>Your Omni Account executes the token transfer</li>
431+
<li>Your Omni Account executes the transfer</li>
375432
</ul>
376433
</div>
377434
</div>

tee-worker/omni-executor/aa-contracts/aa-demo-app/src/lib/aa-utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,51 @@ export function buildTokenTransferUserOp(params: {
796796
});
797797
}
798798

799+
/**
800+
* Build UserOperation for native ETH transfer through OmniAccount
801+
*/
802+
export function buildNativeTransferUserOp(params: {
803+
omniAccountAddress: Address;
804+
recipient: Address;
805+
amount: bigint;
806+
nonce?: bigint;
807+
gasParams?: {
808+
callGasLimit: bigint;
809+
verificationGasLimit: bigint;
810+
preVerificationGas: bigint;
811+
maxFeePerGas: bigint;
812+
maxPriorityFeePerGas: bigint;
813+
};
814+
paymaster?: {
815+
address: Address;
816+
validationGasLimit?: bigint;
817+
postOpGasLimit?: bigint;
818+
data?: `0x${string}`;
819+
};
820+
forGasEstimation?: boolean;
821+
}): UserOperation {
822+
// Build the OmniAccount execute calldata for native ETH transfer
823+
const executeCallData = encodeFunctionData({
824+
abi: CONTRACTS.OmniAccountImplementation.abi,
825+
functionName: 'execute',
826+
args: [
827+
params.recipient, // target: recipient address
828+
params.amount, // value: amount of ETH to send
829+
"0x" as `0x${string}` // data: empty bytes for native transfer
830+
]
831+
});
832+
833+
// Create the UserOperation
834+
return createUserOperation({
835+
sender: params.omniAccountAddress,
836+
nonce: params.nonce,
837+
callData: executeCallData,
838+
gasParams: params.gasParams,
839+
paymaster: params.paymaster,
840+
forGasEstimation: params.forGasEstimation,
841+
});
842+
}
843+
799844
/**
800845
* Estimate gas parameters for a UserOperation using the TEE Worker
801846
* Falls back to local estimation if the worker call fails

tee-worker/omni-executor/aa-contracts/test/EntryPoint.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ contract EntryPointTest is Test {
3939
bytes32 aliceOa = TestUtils.prepare_evm_oa(alice, clientId);
4040

4141
address factory = address(omniAccountFactory);
42-
address sender = 0xB6D24951E90CaCC151Eb9216e516e66eF1BF05A4;
42+
address sender = omniAccountFactory.getAddress(aliceOa, OwnerType.Evm, clientId, rootAddress);
4343

4444
fundAccountOnEntryPoint(sender, entryPoint);
4545

0 commit comments

Comments
 (0)