diff --git a/sdks/smart-wallet-sdk/src/smartWallet.test.ts b/sdks/smart-wallet-sdk/src/smartWallet.test.ts index 18ae1383a..12b983332 100644 --- a/sdks/smart-wallet-sdk/src/smartWallet.test.ts +++ b/sdks/smart-wallet-sdk/src/smartWallet.test.ts @@ -1,15 +1,153 @@ import { ChainId } from '@uniswap/sdk-core' -import { decodeFunctionData } from 'viem' +import { decodeAbiParameters, decodeFunctionData } from 'viem' import abi from '../abis/MinimalDelegationEntry.json' import { ModeType, SMART_WALLET_ADDRESSES } from './constants' import { SmartWallet } from './smartWallet' import { BatchedCall, Call } from './types' +import { BATCHED_CALL_ABI_PARAMS } from './utils/batchedCallPlanner' const EXECUTE_SELECTOR = '0xe9ae5c53' as `0x${string}` +const EXECUTE_USER_OP_SELECTOR = '0x8dd7712f' as `0x${string}` describe('SmartWallet', () => { + describe('encodeUserOp', () => { + it('encodes a single call correctly', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 0n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls) + + expect(result).toBeDefined() + expect(result.calldata).toBeDefined() + expect(result.calldata.startsWith(EXECUTE_USER_OP_SELECTOR)).toBe(true) + expect(result.value).toBe(0n) + }) + + it('encodes multiple calls correctly', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 0n, + }, + { + to: '0x2222222222222222222222222222222222222222', + data: '0x5678', + value: 0n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls) + + expect(result).toBeDefined() + expect(result.calldata).toBeDefined() + expect(result.calldata.startsWith(EXECUTE_USER_OP_SELECTOR)).toBe(true) + expect(result.value).toBe(0n) + }) + + it('sums the value of all calls', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 100n, + }, + { + to: '0x2222222222222222222222222222222222222222', + data: '0x5678', + value: 200n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls) + + expect(result.value).toBe(300n) + }) + + it('encodes with revertOnFailure: true', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 0n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls, { revertOnFailure: true }) + + expect(result).toBeDefined() + expect(result.calldata).toBeDefined() + expect(result.calldata.startsWith(EXECUTE_USER_OP_SELECTOR)).toBe(true) + + // Decode using BATCHED_CALL_ABI_PARAMS which matches the encoding format + const argsData = `0x${result.calldata.slice(10)}` as `0x${string}` + const decoded = decodeAbiParameters(BATCHED_CALL_ABI_PARAMS, argsData) + expect((decoded[0] as BatchedCall).revertOnFailure).toBe(true) + }) + + it('encodes with revertOnFailure: false', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 0n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls, { revertOnFailure: false }) + + expect(result).toBeDefined() + expect(result.calldata).toBeDefined() + + // Decode using BATCHED_CALL_ABI_PARAMS which matches the encoding format + const argsData = `0x${result.calldata.slice(10)}` as `0x${string}` + const decoded = decodeAbiParameters(BATCHED_CALL_ABI_PARAMS, argsData) + expect((decoded[0] as BatchedCall).revertOnFailure).toBe(false) + }) + + it('defaults revertOnFailure to true when no options provided', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 0n, + }, + ] + + const result = SmartWallet.encodeUserOp(calls) + + // Decode using BATCHED_CALL_ABI_PARAMS which matches the encoding format + const argsData = `0x${result.calldata.slice(10)}` as `0x${string}` + const decoded = decodeAbiParameters(BATCHED_CALL_ABI_PARAMS, argsData) + // When no option is provided, BatchedCallPlanner defaults revertOnFailure to true + expect((decoded[0] as BatchedCall).revertOnFailure).toBe(true) + }) + + it('handles calls with chainId property', () => { + const calls: Call[] = [ + { + to: '0x1111111111111111111111111111111111111111', + data: '0x1234', + value: 50n, + chainId: ChainId.SEPOLIA, + }, + ] + + const result = SmartWallet.encodeUserOp(calls) + + expect(result).toBeDefined() + expect(result.calldata.startsWith(EXECUTE_USER_OP_SELECTOR)).toBe(true) + expect(result.value).toBe(50n) + }) + }) + describe('encodeBatchedCall', () => { it('encodes batched call correctly', () => { const calls: Call[] = [ diff --git a/sdks/smart-wallet-sdk/src/smartWallet.ts b/sdks/smart-wallet-sdk/src/smartWallet.ts index 68d0532ed..15a6ddf45 100644 --- a/sdks/smart-wallet-sdk/src/smartWallet.ts +++ b/sdks/smart-wallet-sdk/src/smartWallet.ts @@ -1,5 +1,5 @@ import { ChainId } from '@uniswap/sdk-core' -import { encodeFunctionData } from 'viem' +import { concatHex, encodeFunctionData } from 'viem' import abi from '../abis/MinimalDelegationEntry.json' @@ -12,6 +12,34 @@ import { BatchedCallPlanner } from './utils/batchedCallPlanner' * Main SDK class for interacting with Uniswap smart wallet contracts */ export class SmartWallet { + /** + * Creates method parameters for a UserOperation to be executed through a smart wallet + * @dev Compatible with EntryPoint versions v0.7.0 and v0.8.0 (not v0.6.0) + * + * @param calls Array of calls to encode + * @param options Basic options for the execution + * @returns Method parameters with userOp calldata and value + */ + public static encodeUserOp(calls: Call[], options: ExecuteOptions = {}): MethodParameters { + const planner = new CallPlanner(calls) + const batchedCallPlanner = new BatchedCallPlanner(planner, options.revertOnFailure) + + // UserOp callData format: executeUserOp selector (0x8dd7712f) + abi.encode(abi.encode(Call[]), revertOnFailure) + + // The EntryPoint recognizes this selector and calls executeUserOp(userOp, userOpHash) on the account. + // The account then extracts the execution data from userOp.callData (slicing off the selector). + + // We manually concat the selector + encoded data rather than using encodeFunctionData because + // the callData is not a standard ABI-encoded function call to executeUserOp. + const EXECUTE_USER_OP_SELECTOR = '0x8dd7712f' + const calldata = concatHex([EXECUTE_USER_OP_SELECTOR, batchedCallPlanner.encode()]) + + return { + calldata, + value: planner.value, + } + } + /** * Creates method parameters for executing a simple batch of calls through a smart wallet * @param calls Array of calls to encode