Skip to content

Commit f042bf5

Browse files
authored
Merge pull request #3079 from DFXswiss/develop
Release: develop -> main
2 parents 063c57e + 204a08d commit f042bf5

3 files changed

Lines changed: 80 additions & 56 deletions

File tree

src/integration/blockchain/shared/evm/citrea-base-client.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,40 @@ export abstract class CitreaBaseClient extends EvmClient {
233233
return tx.hash;
234234
}
235235

236+
override async getSwapResult(txId: string, asset: Asset): Promise<number> {
237+
if (asset.type === AssetType.COIN) {
238+
const receipt = await this.getTxReceipt(txId);
239+
const withdrawalTopic = ethers.utils.id('Withdrawal(address,uint256)');
240+
241+
const withdrawalLog = receipt?.logs?.find((l) => l.topics[0] === withdrawalTopic);
242+
if (!withdrawalLog) throw new Error(`Failed to get withdrawal swap result for TX ${txId}`);
243+
244+
return EvmUtil.fromWeiAmount(withdrawalLog.data);
245+
}
246+
247+
return super.getSwapResult(txId, asset);
248+
}
249+
250+
// --- FEE METHODS --- //
251+
252+
override async getTxActualFee(txHash: string): Promise<number> {
253+
// Use raw RPC call to get l1DiffSize and l1FeeRate (not exposed by ethers.js)
254+
const receipt = await this.provider.send('eth_getTransactionReceipt', [txHash]);
255+
256+
const gasUsed = ethers.BigNumber.from(receipt.gasUsed);
257+
const effectiveGasPrice = ethers.BigNumber.from(receipt.effectiveGasPrice);
258+
const l2Fee = gasUsed.mul(effectiveGasPrice);
259+
260+
let l1Fee = ethers.BigNumber.from(0);
261+
if (receipt.l1DiffSize && receipt.l1FeeRate) {
262+
const l1DiffSize = ethers.BigNumber.from(receipt.l1DiffSize);
263+
const l1FeeRate = ethers.BigNumber.from(receipt.l1FeeRate);
264+
l1Fee = l1DiffSize.mul(l1FeeRate);
265+
}
266+
267+
return EvmUtil.fromWeiAmount(l1Fee.add(l2Fee));
268+
}
269+
236270
// --- TRADING INTEGRATION --- //
237271

238272
override async getPoolAddress(asset1: Asset, asset2: Asset, poolFee: FeeAmount): Promise<string> {

src/integration/blockchain/shared/evm/evm-client.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,7 @@ export abstract class EvmClient extends BlockchainClient {
670670
);
671671
if (!swapLog) throw new Error(`Failed to get swap result for TX ${txId}`);
672672

673-
const token = await this.getToken(asset);
674-
return EvmUtil.fromWeiAmount(swapLog.data, token.decimals);
673+
return EvmUtil.fromWeiAmount(swapLog.data, asset.decimals);
675674
}
676675

677676
private async getRoute(source: Asset, target: Asset, sourceAmount: number, maxSlippage: number): Promise<SwapRoute> {

src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts

Lines changed: 45 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@ import { EVM_CHAIN_CONFIG, getEvmChainConfig, isEvmBlockchainSupported } from '.
1313
// Source: https://github.com/MetaMask/delegation-framework
1414
const METAMASK_DELEGATOR_ADDRESS = '0x63c0c19a282a1b52b07dd5a65b58948a07dae32b' as Address;
1515

16-
// ERC-4337 EntryPoint v0.7 - canonical address on all chains
17-
const ENTRY_POINT_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032' as Address;
18-
19-
// EIP-7702 factory marker - signals to bundler that this is an EIP-7702 UserOperation
20-
const EIP7702_FACTORY = '0x0000000000000000000000000000000000007702' as Address;
16+
// ERC-4337 EntryPoint v0.8 - required for EIP-7702 support
17+
const ENTRY_POINT_V08 = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108' as Address;
2118

2219
// MetaMask Delegator ABI - ERC-7821 BatchExecutor interface
2320
const DELEGATOR_ABI = parseAbi(['function execute((bytes32 mode, bytes executionData) execution) external payable']);
@@ -40,11 +37,11 @@ export interface GaslessTransferResult {
4037
userOpHash: string;
4138
}
4239

43-
interface UserOperationV07 {
40+
interface UserOperationV08 {
4441
sender: Address;
4542
nonce: Hex;
46-
factory: Address;
47-
factoryData: Hex;
43+
factory?: Address | null;
44+
factoryData?: Hex;
4845
callData: Hex;
4946
callGasLimit: Hex;
5047
verificationGasLimit: Hex;
@@ -56,6 +53,16 @@ interface UserOperationV07 {
5653
paymasterPostOpGasLimit: Hex;
5754
paymasterData: Hex;
5855
signature: Hex;
56+
// EIP-7702 authorization - separate field per ERC-7769
57+
// Pimlico expects 'contractAddress' not 'address'
58+
eip7702Auth?: {
59+
contractAddress: Address;
60+
chainId: Hex;
61+
nonce: Hex;
62+
r: Hex;
63+
s: Hex;
64+
yParity: Hex;
65+
};
5966
}
6067

6168
@Injectable()
@@ -234,19 +241,17 @@ export class PimlicoBundlerService {
234241
// 2. Encode the execute() call for MetaMask Delegator (ERC-7821 format)
235242
const callData = this.encodeExecuteCall(token.chainId as Address, transferData);
236243

237-
// 3. Encode the EIP-7702 authorization as factoryData
238-
const factoryData = this.encodeAuthorizationAsFactoryData(authorization);
239-
240-
// 4. Build the UserOperation
241-
const userOp = await this.buildUserOperation(userAddress as Address, callData, factoryData, pimlicoUrl);
244+
// 3. Build the UserOperation with EIP-7702 authorization
245+
// Authorization is included before gas estimation for accurate estimates
246+
const userOp = await this.buildUserOperation(userAddress as Address, callData, authorization, pimlicoUrl);
242247

243-
// 5. Sponsor the UserOperation via Pimlico Paymaster
248+
// 4. Sponsor the UserOperation via Pimlico Paymaster
244249
const sponsoredUserOp = await this.sponsorUserOperation(userOp, pimlicoUrl);
245250

246-
// 6. Submit the UserOperation via Pimlico Bundler
251+
// 5. Submit the UserOperation via Pimlico Bundler
247252
const userOpHash = await this.sendUserOperation(sponsoredUserOp, pimlicoUrl);
248253

249-
// 7. Wait for the transaction to be mined
254+
// 6. Wait for the transaction to be mined
250255
const txHash = await this.waitForUserOperation(userOpHash, pimlicoUrl);
251256

252257
return { txHash, userOpHash };
@@ -298,49 +303,25 @@ export class PimlicoBundlerService {
298303
}
299304

300305
/**
301-
* Encode EIP-7702 authorization as factoryData for UserOperation
302-
*
303-
* When factory = 0x7702, the bundler expects factoryData to contain
304-
* the signed EIP-7702 authorization that delegates the smart account
305-
* implementation to the EOA.
306-
*/
307-
private encodeAuthorizationAsFactoryData(authorization: Eip7702Authorization): Hex {
308-
// factoryData format for EIP-7702:
309-
// abi.encodePacked(address delegatee, uint256 nonce, bytes signature)
310-
// where signature = abi.encodePacked(r, s, yParity)
311-
const signature = concat([
312-
authorization.r as Hex,
313-
authorization.s as Hex,
314-
toHex(authorization.yParity, { size: 1 }),
315-
]);
316-
317-
return concat([
318-
authorization.address as Hex, // delegatee (MetaMask Delegator)
319-
pad(toHex(BigInt(authorization.nonce)), { size: 32 }), // nonce
320-
signature, // signature (r, s, yParity)
321-
]);
322-
}
323-
324-
/**
325-
* Build UserOperation v0.7 structure
306+
* Build UserOperation v0.8 structure for EIP-7702
307+
* Note: factory is intentionally left null/undefined - Pimlico expects this for EIP-7702
326308
*/
327309
private async buildUserOperation(
328310
sender: Address,
329311
callData: Hex,
330-
factoryData: Hex,
312+
authorization: Eip7702Authorization,
331313
pimlicoUrl: string,
332-
): Promise<UserOperationV07> {
314+
): Promise<UserOperationV08> {
333315
// Get current gas prices from Pimlico
334316
const gasPrice = await this.getGasPrice(pimlicoUrl);
335317

336318
// Get sender nonce from EntryPoint
337319
const nonce = await this.getSenderNonce(sender, pimlicoUrl);
338320

339-
const userOp: UserOperationV07 = {
321+
const userOp: UserOperationV08 = {
340322
sender,
341323
nonce: toHex(nonce),
342-
factory: EIP7702_FACTORY,
343-
factoryData,
324+
// For EIP-7702, do NOT set factory - Pimlico expects it to be null/undefined
344325
callData,
345326
callGasLimit: toHex(200000n),
346327
verificationGasLimit: toHex(500000n),
@@ -352,9 +333,19 @@ export class PimlicoBundlerService {
352333
paymasterPostOpGasLimit: toHex(0n),
353334
paymasterData: '0x' as Hex,
354335
signature: '0x' as Hex, // Will be filled by sponsorship or left empty for EIP-7702
336+
// EIP-7702 authorization must be included BEFORE gas estimation
337+
// Pimlico expects 'contractAddress' not 'address'
338+
eip7702Auth: {
339+
contractAddress: authorization.address as Address,
340+
chainId: toHex(authorization.chainId),
341+
nonce: toHex(authorization.nonce),
342+
r: authorization.r as Hex,
343+
s: authorization.s as Hex,
344+
yParity: toHex(authorization.yParity),
345+
},
355346
};
356347

357-
// Estimate gas limits
348+
// Estimate gas limits (now includes eip7702Auth)
358349
const estimated = await this.estimateUserOperationGas(userOp, pimlicoUrl);
359350
userOp.callGasLimit = estimated.callGasLimit;
360351
userOp.verificationGasLimit = estimated.verificationGasLimit;
@@ -366,8 +357,8 @@ export class PimlicoBundlerService {
366357
/**
367358
* Sponsor UserOperation via Pimlico Paymaster
368359
*/
369-
private async sponsorUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise<UserOperationV07> {
370-
const response = await this.jsonRpc(pimlicoUrl, 'pm_sponsorUserOperation', [userOp, ENTRY_POINT_V07]);
360+
private async sponsorUserOperation(userOp: UserOperationV08, pimlicoUrl: string): Promise<UserOperationV08> {
361+
const response = await this.jsonRpc(pimlicoUrl, 'pm_sponsorUserOperation', [userOp, ENTRY_POINT_V08]);
371362

372363
return {
373364
...userOp,
@@ -384,8 +375,8 @@ export class PimlicoBundlerService {
384375
/**
385376
* Submit UserOperation to Pimlico Bundler
386377
*/
387-
private async sendUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise<string> {
388-
return this.jsonRpc(pimlicoUrl, 'eth_sendUserOperation', [userOp, ENTRY_POINT_V07]);
378+
private async sendUserOperation(userOp: UserOperationV08, pimlicoUrl: string): Promise<string> {
379+
return this.jsonRpc(pimlicoUrl, 'eth_sendUserOperation', [userOp, ENTRY_POINT_V08]);
389380
}
390381

391382
/**
@@ -435,7 +426,7 @@ export class PimlicoBundlerService {
435426
try {
436427
const response = await this.jsonRpc(pimlicoUrl, 'eth_call', [
437428
{
438-
to: ENTRY_POINT_V07,
429+
to: ENTRY_POINT_V08,
439430
data: encodeFunctionData({
440431
abi: parseAbi(['function getNonce(address sender, uint192 key) view returns (uint256)']),
441432
functionName: 'getNonce',
@@ -454,11 +445,11 @@ export class PimlicoBundlerService {
454445
* Estimate gas for UserOperation
455446
*/
456447
private async estimateUserOperationGas(
457-
userOp: UserOperationV07,
448+
userOp: UserOperationV08,
458449
pimlicoUrl: string,
459450
): Promise<{ callGasLimit: Hex; verificationGasLimit: Hex; preVerificationGas: Hex }> {
460451
try {
461-
const response = await this.jsonRpc(pimlicoUrl, 'eth_estimateUserOperationGas', [userOp, ENTRY_POINT_V07]);
452+
const response = await this.jsonRpc(pimlicoUrl, 'eth_estimateUserOperationGas', [userOp, ENTRY_POINT_V08]);
462453
return {
463454
callGasLimit: response.callGasLimit,
464455
verificationGasLimit: response.verificationGasLimit,

0 commit comments

Comments
 (0)