Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const SmartAccountConfirmPage: FunctionComponent = observer(() => {
insufficientBalance,
balanceLoaded,
retry,
} = useSmartAccountFee(chainId, vaultId, hexAddress, isValid);
} = useSmartAccountFee(chainId, hexAddress, isValid);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { EstimateSmartAccountFeeMsg } from "@keplr-wallet/background";
import { InExtensionMessageRequester } from "@keplr-wallet/router-extension";
import { BACKGROUND_PORT } from "@keplr-wallet/router";
import { CoinPretty, Int } from "@keplr-wallet/unit";
import {
ALLOWED_DELEGATORS,
buildDummyAuthorizationList,
} from "@keplr-wallet/background";
import { computeEIP1559TxFees } from "@keplr-wallet/hooks-evm";
import { CoinPretty, Dec, Int } from "@keplr-wallet/unit";
import { useStore } from "../../../../../stores";
import { formatFee } from "../../utils";

type FeeState =
| { status: "loading" }
| { status: "success"; estimatedFeeWei: string }
| { status: "success"; gasUsed: number }
| { status: "error" };

export function useSmartAccountFee(
chainId: string,
vaultId: string,
hexAddress: string,
isValid: boolean
) {
const { chainStore, queriesStore, priceStore } = useStore();
const { chainStore, queriesStore, priceStore, ethereumAccountStore } =
useStore();

const nativeCurrency = useMemo(() => {
try {
Expand All @@ -30,46 +32,81 @@ export function useSmartAccountFee(
}
}, [chainStore, chainId]);

const evmChainId = useMemo(() => {
try {
Comment thread
piatoss3612 marked this conversation as resolved.
Outdated
const unwrapped = chainStore.getModularChain(chainId).unwrapped;
if (unwrapped.type === "evm") return unwrapped.evm.chainId;
if (unwrapped.type === "ethermint") return unwrapped.evm.chainId;
return 0;
} catch {
return 0;
}
}, [chainStore, chainId]);

const nativeSymbol = nativeCurrency?.coinDenom ?? "ETH";

const [feeState, setFeeState] = useState<FeeState>({ status: "loading" });
const [retryCount, setRetryCount] = useState(0);

useEffect(() => {
if (!isValid) return;
if (!isValid || !hexAddress || !evmChainId) return;
let cancelled = false;
setFeeState({ status: "loading" });
new InExtensionMessageRequester()
.sendMessage(
BACKGROUND_PORT,
new EstimateSmartAccountFeeMsg(chainId, vaultId)
)

const ethereumAccount = ethereumAccountStore.getAccount(chainId);

ethereumAccount
.simulateGas(hexAddress, {
to: hexAddress,
value: "0x0",
data: "0x",
authorizationList: buildDummyAuthorizationList(
ALLOWED_DELEGATORS[0],
evmChainId
),
})
.then((result) => {
if (!cancelled)
setFeeState({
status: "success",
estimatedFeeWei: result.estimatedFeeWei,
});
if (!cancelled) {
setFeeState({ status: "success", gasUsed: result.gasUsed });
}
Comment thread
piatoss3612 marked this conversation as resolved.
})
.catch(() => {
if (!cancelled) setFeeState({ status: "error" });
});

return () => {
cancelled = true;
};
}, [chainId, vaultId, isValid, retryCount]);
}, [
chainId,
hexAddress,
evmChainId,
isValid,
retryCount,
ethereumAccountStore,
]);

// Fee from reactive queries (feeHistory percentile + baseFee margin, fillUnsignedEVMTx์™€ ๋™์ผ ๋กœ์ง)
const ethereumQueries = queriesStore.get(chainId).ethereum;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard fee-query setup behind chain validation

This new unconditional lookup queriesStore.get(chainId).ethereum runs even when the confirm route params are invalid, but useConfirmRoute only checks for non-empty strings and redirects asynchronously in an effect. If chainId is missing/stale (or otherwise unknown), constructing these Ethereum queries calls chainStore.getModularChain(chainId) and throws, so the confirm page crashes before redirect instead of failing gracefully. Please gate this lookup with the same defensive checks used above (or wrap it in try/catch) so invalid URLs don't hard-crash the page.

Useful? React with ๐Ÿ‘ย / ๐Ÿ‘Ž.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queriesStore.get(chainId)๋Š” ์ž˜๋ชป๋œ chainId์—๋„ throwํ•˜์ง€ ์•Š๊ณ  lazyํ•˜๊ฒŒ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋ฐ”๋กœ ๋‹ค์Œ ์ค„์—์„œ ethereumQueries ? ... : undefined๋กœ ๊ฐ€๋“œ๋˜์–ด ์žˆ๊ณ , isValid false์ผ ๋•Œ useRedirectIfInvalid + useEffect early return์œผ๋กœ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

const txFees = ethereumQueries
? computeEIP1559TxFees(ethereumQueries, "average", chainId)
: undefined;

const estimatedFeeWei = useMemo(() => {
if (feeState.status !== "success") return null;
const feePerGas = txFees?.maxFeePerGas ?? txFees?.gasPrice;
if (!feePerGas || feePerGas.isZero()) return null;
const gasLimit = Math.ceil(feeState.gasUsed * 1.3);
Comment on lines +115 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark missing gas-price data as fee-estimation failure

When gas simulation succeeds but fee queries are still empty or errored, feePerGas becomes zero/undefined and this path returns null instead of transitioning to an error state. In that case isFailed stays false, so the confirm page can enable Approve while fee text shows failure and insufficientBalance is skipped (because estimatedFeeWei is null), which can lead to avoidable upgrade/downgrade tx failures from underfunded accounts.

Useful? React with ๐Ÿ‘ย / ๐Ÿ‘Ž.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. !feeDisplay๋ฅผ approveDisabled ์กฐ๊ฑด์— ์ถ”๊ฐ€ํ•˜์—ฌ, fee ์ฟผ๋ฆฌ๊ฐ€ ๋กœ๋“œ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ์Šน์ธ ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

cb42016ad

Comment on lines +115 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark missing fee-per-gas data as estimation failure

When gas simulation succeeds but fee queries are still empty, this branch returns null and keeps feeState in success. In this commit confirm.tsx also disables approval on !feeDisplay, so users can end up with a permanently disabled Approve button and no retry path (isFailed stays false) whenever eth_feeHistory/eth_gasPrice data is temporarily unavailable, even though the request itself is valid.

Useful? React with ๐Ÿ‘ย / ๐Ÿ‘Ž.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

txFees๋Š” MobX reactive query ๊ธฐ๋ฐ˜์ด๋ผ RPC ์‘๋‹ต ์˜ค๋ฉด ์ž๋™ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค. ์˜๊ตฌ ๊ต์ฐฉ์€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

const fee = feePerGas.mul(new Dec(gasLimit)).truncate();
return "0x" + BigInt(fee.toString()).toString(16);
}, [feeState, txFees]);

const estimatedFeeWei =
feeState.status === "success" ? feeState.estimatedFeeWei : null;
const isEstimating = feeState.status === "loading";
const isFailed = feeState.status === "error";

const feeDisplay = useMemo(
() =>
feeState.status === "success"
? formatFee(feeState.estimatedFeeWei, nativeSymbol)
: null,
[feeState, nativeSymbol]
() => (estimatedFeeWei ? formatFee(estimatedFeeWei, nativeSymbol) : null),
[estimatedFeeWei, nativeSymbol]
);

const feeUsd = useMemo(() => {
Expand Down
7 changes: 0 additions & 7 deletions packages/background/src/keyring-ethereum/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
CheckNeedEnableAccessForEVMMsg,
UpgradeSmartAccountMsg,
DowngradeSmartAccountMsg,
EstimateSmartAccountFeeMsg,
GetEthereumAddressForVaultMsg,
} from "./messages";
import { KeyRingEthereumService } from "./service";
Expand Down Expand Up @@ -59,12 +58,6 @@ export const getHandler: (
(msg as DowngradeSmartAccountMsg).chainId,
(msg as DowngradeSmartAccountMsg).vaultId
);
case EstimateSmartAccountFeeMsg:
return service.estimateSmartAccountFee(
env,
(msg as EstimateSmartAccountFeeMsg).chainId,
(msg as EstimateSmartAccountFeeMsg).vaultId
);
case GetEthereumAddressForVaultMsg:
return service.getEthAddressForVault(
(msg as GetEthereumAddressForVaultMsg).chainId,
Expand Down
62 changes: 62 additions & 0 deletions packages/background/src/keyring-ethereum/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
WalletSendCallsRequest,
} from "@keplr-wallet/types";
import { Address, Hex } from "ox";
import { Execute } from "ox/erc7821";
import { Buffer } from "buffer/";
import { Authorization } from "ox";

import {
ALLOWED_DELEGATORS,
EIP5792_ERROR_UNSUPPORTED_CAPABILITY,
RPC_ERROR_INVALID_PARAMS,
RPC_ERROR_UNRECOGNIZED_CHAIN,
Expand Down Expand Up @@ -124,6 +126,66 @@ export function buildDummyAuthorizationList(
];
}

// โ”€โ”€ Atomic Batch TX Assembly โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/**
* EIP-5792 calls ๋ฐฐ์—ด์„ StatelessDeleGator์˜ execute(bytes32,bytes) calldata๋กœ ๋ณ€ํ™˜.
* ox/erc7821 Execute.encodeData๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ERC-7579 ํ‘œ์ค€ ์ค€์ˆ˜๋ฅผ ๋ณด์žฅ.
* nonce/gas/fee๋Š” ํฌํ•จํ•˜์ง€ ์•Š์Œ โ€” fillUnsignedEVMTx๊ฐ€ ๋‹ด๋‹น.
*/
export function createAtomicBatchTransaction(
calls: EIP5792Call[],
selectedAddress: string,
delegationStatus: DelegationStatus,
evmChainId: number
): UnsignedEVMTransaction {
const data = Execute.encodeData(
calls.map((call, i) => {
if (!call.to) {
throw new Error(
`Call at index ${i}: contract deployment is not supported in batch execution`
);
}
return {
to: call.to as `0x${string}`,
value: BigInt(call.value ?? "0x0"),
data: (call.data ?? "0x") as `0x${string}`,
};
})
);

const tx: UnsignedEVMTransaction = {
to: selectedAddress,
data,
value: "0x0",
chainId: evmChainId,
};

if (delegationStatus === "ready") {
tx.authorizationList = buildDummyAuthorizationList(
ALLOWED_DELEGATORS[0],
evmChainId
);
}

return tx;
}

/**
* ๋ฏธ์—…๊ทธ๋ ˆ์ด๋“œ ๊ณ„์ •์˜ ๊ฐ€์Šค ์ถ”์ •์„ ์œ„ํ•œ stateOverride ์ƒ์„ฑ.
* eth_estimateGas 3๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•˜์—ฌ delegation ์ฝ”๋“œ๋ฅผ ์ฃผ์ž….
*/
export function buildDelegationStateOverride(
address: string,
delegatorAddress: string
): Record<string, { code: string }> {
return {
[address]: {
code: EIP7702_DELEGATION_PREFIX + delegatorAddress.toLowerCase().slice(2),
},
};
}

// โ”€โ”€ Types โ”€โ”€

export interface EVMTransactionParam {
Expand Down
5 changes: 5 additions & 0 deletions packages/background/src/keyring-ethereum/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export * from "./messages";
export * from "./service";
export { ALLOWED_DELEGATORS } from "./constants";
export {
buildDummyAuthorizationList,
createAtomicBatchTransaction,
buildDelegationStateOverride,
} from "./helper";
2 changes: 0 additions & 2 deletions packages/background/src/keyring-ethereum/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
CheckNeedEnableAccessForEVMMsg,
UpgradeSmartAccountMsg,
DowngradeSmartAccountMsg,
EstimateSmartAccountFeeMsg,
GetEthereumAddressForVaultMsg,
} from "./messages";
import { ROUTE } from "./constants";
Expand All @@ -25,7 +24,6 @@ export function init(
router.registerMessage(CheckNeedEnableAccessForEVMMsg);
router.registerMessage(UpgradeSmartAccountMsg);
router.registerMessage(DowngradeSmartAccountMsg);
router.registerMessage(EstimateSmartAccountFeeMsg);
router.registerMessage(GetEthereumAddressForVaultMsg);

router.addHandler(ROUTE, getHandler(service, permissionInteractionService));
Expand Down
39 changes: 0 additions & 39 deletions packages/background/src/keyring-ethereum/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,45 +179,6 @@ export class DowngradeSmartAccountMsg extends Message<string> {
}
}

export class EstimateSmartAccountFeeMsg extends Message<{
gasLimit: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
estimatedFeeWei: string;
}> {
public static type() {
return "estimate-smart-account-fee";
}

constructor(
public readonly chainId: string,
public readonly vaultId: string
) {
super();
}

validateBasic(): void {
if (!this.chainId) {
throw new Error("chain id not set");
}
if (!this.vaultId) {
throw new Error("vault id not set");
}
}

override approveExternal(): boolean {
return false;
}

route(): string {
return ROUTE;
}

type(): string {
return EstimateSmartAccountFeeMsg.type();
}
}

export class GetEthereumAddressForVaultMsg extends Message<string> {
public static type() {
return "get-ethereum-address-for-vault";
Expand Down
Loading
Loading