From 6a871972baf302c1b2e6065b6f179dc341b32c67 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Mon, 28 Jul 2025 07:14:18 +1200 Subject: [PATCH] [SDK] Fallback to onchain nonce in 7702 execution --- .changeset/silly-bugs-clean.md | 5 + .../7702-smart-account.tsx | 1 + .../src/transaction/actions/estimate-gas.ts | 56 +- .../in-app/core/eip7702/minimal-account.ts | 634 +++++++++++++++++- 4 files changed, 650 insertions(+), 46 deletions(-) create mode 100644 .changeset/silly-bugs-clean.md diff --git a/.changeset/silly-bugs-clean.md b/.changeset/silly-bugs-clean.md new file mode 100644 index 00000000000..3c22c524ad5 --- /dev/null +++ b/.changeset/silly-bugs-clean.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fallback to onchain nonce in 7702 execution diff --git a/apps/playground-web/src/components/account-abstraction/7702-smart-account.tsx b/apps/playground-web/src/components/account-abstraction/7702-smart-account.tsx index c097a81e300..34bbe01c6e7 100644 --- a/apps/playground-web/src/components/account-abstraction/7702-smart-account.tsx +++ b/apps/playground-web/src/components/account-abstraction/7702-smart-account.tsx @@ -101,6 +101,7 @@ export function Eip7702SmartAccountPreview() { setTxHash(null); }} onError={(error) => { + console.error("minting error", error); alert(`Error: ${error.message}`); }} onTransactionSent={async (tx) => { diff --git a/packages/thirdweb/src/transaction/actions/estimate-gas.ts b/packages/thirdweb/src/transaction/actions/estimate-gas.ts index 737052de088..9aad05946f2 100644 --- a/packages/thirdweb/src/transaction/actions/estimate-gas.ts +++ b/packages/thirdweb/src/transaction/actions/estimate-gas.ts @@ -2,7 +2,6 @@ import * as ox__Hex from "ox/Hex"; import { formatTransactionRequest } from "viem"; import { roundUpGas } from "../../gas/op-gas-fee-reducer.js"; import { getAddress } from "../../utils/address.js"; -import { hexToBytes } from "../../utils/encoding/to-bytes.js"; import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { Account } from "../../wallets/interfaces/wallet.js"; @@ -117,31 +116,20 @@ export async function estimateGas( const rpcRequest = getRpcClient(options.transaction); try { - let gas = await eth_estimateGas( - rpcRequest, - formatTransactionRequest({ - authorizationList: authorizationList?.map((auth) => ({ - ...auth, - contractAddress: getAddress(auth.address), - nonce: Number(auth.nonce), - r: ox__Hex.fromNumber(auth.r), - s: ox__Hex.fromNumber(auth.s), - })), - data: encodedData, - from: fromAddress ? getAddress(fromAddress) : undefined, - to: toAddress ? getAddress(toAddress) : undefined, - value, - ...(authorizationList && authorizationList?.length > 0 - ? { - gas: - minGas( - hexToBytes(encodedData), - BigInt(authorizationList?.length ?? 0), - ) + 100_000n, - } - : {}), - }), - ); + const formattedTx = formatTransactionRequest({ + authorizationList: authorizationList?.map((auth) => ({ + ...auth, + contractAddress: getAddress(auth.address), + nonce: Number(auth.nonce), + r: ox__Hex.fromNumber(auth.r), + s: ox__Hex.fromNumber(auth.s), + })), + data: encodedData, + from: fromAddress ? getAddress(fromAddress) : undefined, + to: toAddress ? getAddress(toAddress) : undefined, + value, + }); + let gas = await eth_estimateGas(rpcRequest, formattedTx); if (options.transaction.chain.experimental?.increaseZeroByteCount) { gas = roundUpGas(gas); @@ -158,19 +146,3 @@ export async function estimateGas( cache.set(txWithFrom, promise); return promise; } - -// EIP-7623 + EIP-7702 floor calculation -const TxGas = 21_000n; -const TxCostFloorPerToken = 10n; // params.TxCostFloorPerToken -const TxTokenPerNonZero = 4n; // params.TxTokenPerNonZeroByte -const TxAuthTupleGas = 12_500n; - -function minGas(data: Uint8Array, authCount = 0n) { - let nz = 0n; - for (const b of data) if (b !== 0) nz++; - const z = BigInt(data.length) - nz; - const tokens = nz * TxTokenPerNonZero + z; - const floor = TxGas + tokens * TxCostFloorPerToken; - const intrinsic = TxGas + authCount * TxAuthTupleGas; - return floor > intrinsic ? floor : intrinsic; -} diff --git a/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts b/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts index b23be9f5ca4..262cc263a50 100644 --- a/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts +++ b/packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts @@ -1,5 +1,6 @@ import type { Definition, TypedData } from "ox/TypedData"; import type { Hex, SignableMessage } from "viem"; +import type { Chain } from "../../../../chains/types.js"; import { getCachedChain } from "../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { getBytecode } from "../../../../contract/actions/get-bytecode.js"; @@ -8,6 +9,7 @@ import { type ThirdwebContract, } from "../../../../contract/contract.js"; import { execute } from "../../../../extensions/erc7702/__generated__/MinimalAccount/write/execute.js"; +import { getRpcClient } from "../../../../rpc/rpc.js"; import type { SignedAuthorization } from "../../../../transaction/actions/eip7702/authorization.js"; import { toSerializableTransaction } from "../../../../transaction/actions/to-serializable-transaction.js"; import type { SendTransactionResult } from "../../../../transaction/types.js"; @@ -43,15 +45,23 @@ export const create7702MinimalAccount = (args: { address: adminAccount.address, chain, client, + abi: MinimalAccountAbi, }); // check if account has been delegated already let authorization: SignedAuthorization | undefined; const isMinimalAccount = await is7702MinimalAccount(eoaContract); if (!isMinimalAccount) { // if not, sign authorization - const nonce = firstTx.nonce - ? BigInt(firstTx.nonce) + (sponsorGas ? 0n : 1n) - : 0n; // TODO (7702): get remote nonce if not provided, should be in the tx though + let nonce = firstTx.nonce + ? BigInt(firstTx.nonce) + : BigInt( + await getNonce({ + client, + address: adminAccount.address, + chain: getCachedChain(firstTx.chainId), + }), + ); + nonce += sponsorGas ? 0n : 1n; const auth = await adminAccount.signAuthorization?.({ address: MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS, chainId: firstTx.chainId, @@ -170,8 +180,29 @@ export const create7702MinimalAccount = (args: { return minimalAccount; }; +async function getNonce(args: { + client: ThirdwebClient; + address: string; + chain: Chain; +}): Promise { + const { client, address, chain } = args; + const rpcRequest = getRpcClient({ + chain, + client, + }); + const nonce = await import( + "../../../../rpc/actions/eth_getTransactionCount.js" + ).then(({ eth_getTransactionCount }) => + eth_getTransactionCount(rpcRequest, { + address, + blockTag: "pending", + }), + ); + return nonce; +} + async function is7702MinimalAccount( - eoaContract: ThirdwebContract, + eoaContract: ThirdwebContract, ): Promise { const code = await getBytecode(eoaContract); const isDelegated = code.length > 0 && code.startsWith("0xef0100"); @@ -206,3 +237,598 @@ async function waitForTransactionHash(args: { `Timeout waiting for transaction to be mined on chain ${args.options.chain.id} with transactionId: ${args.transactionId}`, ); } + +const MinimalAccountAbi = [ + { type: "receive", stateMutability: "payable" }, + { + type: "function", + name: "createSessionWithSig", + inputs: [ + { + name: "sessionSpec", + type: "tuple", + internalType: "struct SessionLib.SessionSpec", + components: [ + { name: "signer", type: "address", internalType: "address" }, + { name: "isWildcard", type: "bool", internalType: "bool" }, + { name: "expiresAt", type: "uint256", internalType: "uint256" }, + { + name: "callPolicies", + type: "tuple[]", + internalType: "struct SessionLib.CallSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { + name: "maxValuePerUse", + type: "uint256", + internalType: "uint256", + }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + internalType: "struct SessionLib.Constraint[]", + components: [ + { + name: "condition", + type: "uint8", + internalType: "enum SessionLib.Condition", + }, + { name: "index", type: "uint64", internalType: "uint64" }, + { + name: "refValue", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "limit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { + name: "limit", + type: "uint256", + internalType: "uint256", + }, + { + name: "period", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + }, + ], + }, + { + name: "transferPolicies", + type: "tuple[]", + internalType: "struct SessionLib.TransferSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { + name: "maxValuePerUse", + type: "uint256", + internalType: "uint256", + }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + ], + }, + { name: "uid", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "signature", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "eip712Domain", + inputs: [], + outputs: [ + { name: "fields", type: "bytes1", internalType: "bytes1" }, + { name: "name", type: "string", internalType: "string" }, + { name: "version", type: "string", internalType: "string" }, + { name: "chainId", type: "uint256", internalType: "uint256" }, + { name: "verifyingContract", type: "address", internalType: "address" }, + { name: "salt", type: "bytes32", internalType: "bytes32" }, + { name: "extensions", type: "uint256[]", internalType: "uint256[]" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "execute", + inputs: [ + { + name: "calls", + type: "tuple[]", + internalType: "struct Call[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "value", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "executeWithSig", + inputs: [ + { + name: "wrappedCalls", + type: "tuple", + internalType: "struct WrappedCalls", + components: [ + { + name: "calls", + type: "tuple[]", + internalType: "struct Call[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "value", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + }, + { name: "uid", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "signature", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "getCallPoliciesForSigner", + inputs: [{ name: "signer", type: "address", internalType: "address" }], + outputs: [ + { + name: "", + type: "tuple[]", + internalType: "struct SessionLib.CallSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { name: "maxValuePerUse", type: "uint256", internalType: "uint256" }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + internalType: "struct SessionLib.Constraint[]", + components: [ + { + name: "condition", + type: "uint8", + internalType: "enum SessionLib.Condition", + }, + { name: "index", type: "uint64", internalType: "uint64" }, + { name: "refValue", type: "bytes32", internalType: "bytes32" }, + { + name: "limit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getSessionExpirationForSigner", + inputs: [{ name: "signer", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getSessionStateForSigner", + inputs: [{ name: "signer", type: "address", internalType: "address" }], + outputs: [ + { + name: "", + type: "tuple", + internalType: "struct SessionLib.SessionState", + components: [ + { + name: "transferValue", + type: "tuple[]", + internalType: "struct SessionLib.LimitState[]", + components: [ + { name: "remaining", type: "uint256", internalType: "uint256" }, + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { name: "index", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "callValue", + type: "tuple[]", + internalType: "struct SessionLib.LimitState[]", + components: [ + { name: "remaining", type: "uint256", internalType: "uint256" }, + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { name: "index", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "callParams", + type: "tuple[]", + internalType: "struct SessionLib.LimitState[]", + components: [ + { name: "remaining", type: "uint256", internalType: "uint256" }, + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { name: "index", type: "uint256", internalType: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getTransferPoliciesForSigner", + inputs: [{ name: "signer", type: "address", internalType: "address" }], + outputs: [ + { + name: "", + type: "tuple[]", + internalType: "struct SessionLib.TransferSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "maxValuePerUse", type: "uint256", internalType: "uint256" }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isWildcardSigner", + inputs: [{ name: "signer", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "onERC1155BatchReceived", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "address" }, + { name: "", type: "uint256[]", internalType: "uint256[]" }, + { name: "", type: "uint256[]", internalType: "uint256[]" }, + { name: "", type: "bytes", internalType: "bytes" }, + ], + outputs: [{ name: "", type: "bytes4", internalType: "bytes4" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onERC1155Received", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "address" }, + { name: "", type: "uint256", internalType: "uint256" }, + { name: "", type: "uint256", internalType: "uint256" }, + { name: "", type: "bytes", internalType: "bytes" }, + ], + outputs: [{ name: "", type: "bytes4", internalType: "bytes4" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onERC721Received", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "address" }, + { name: "", type: "uint256", internalType: "uint256" }, + { name: "", type: "bytes", internalType: "bytes" }, + ], + outputs: [{ name: "", type: "bytes4", internalType: "bytes4" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "supportsInterface", + inputs: [{ name: "interfaceId", type: "bytes4", internalType: "bytes4" }], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "event", + name: "Executed", + inputs: [ + { name: "to", type: "address", indexed: true, internalType: "address" }, + { + name: "value", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { name: "data", type: "bytes", indexed: false, internalType: "bytes" }, + ], + anonymous: false, + }, + { + type: "event", + name: "SessionCreated", + inputs: [ + { + name: "signer", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sessionSpec", + type: "tuple", + indexed: false, + internalType: "struct SessionLib.SessionSpec", + components: [ + { name: "signer", type: "address", internalType: "address" }, + { name: "isWildcard", type: "bool", internalType: "bool" }, + { name: "expiresAt", type: "uint256", internalType: "uint256" }, + { + name: "callPolicies", + type: "tuple[]", + internalType: "struct SessionLib.CallSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + { + name: "maxValuePerUse", + type: "uint256", + internalType: "uint256", + }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + internalType: "struct SessionLib.Constraint[]", + components: [ + { + name: "condition", + type: "uint8", + internalType: "enum SessionLib.Condition", + }, + { name: "index", type: "uint64", internalType: "uint64" }, + { + name: "refValue", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "limit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { + name: "limit", + type: "uint256", + internalType: "uint256", + }, + { + name: "period", + type: "uint256", + internalType: "uint256", + }, + ], + }, + ], + }, + ], + }, + { + name: "transferPolicies", + type: "tuple[]", + internalType: "struct SessionLib.TransferSpec[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { + name: "maxValuePerUse", + type: "uint256", + internalType: "uint256", + }, + { + name: "valueLimit", + type: "tuple", + internalType: "struct SessionLib.UsageLimit", + components: [ + { + name: "limitType", + type: "uint8", + internalType: "enum SessionLib.LimitType", + }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint256", internalType: "uint256" }, + ], + }, + ], + }, + { name: "uid", type: "bytes32", internalType: "bytes32" }, + ], + }, + ], + anonymous: false, + }, + { + type: "event", + name: "ValueReceived", + inputs: [ + { name: "from", type: "address", indexed: true, internalType: "address" }, + { + name: "value", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "AllowanceExceeded", + inputs: [ + { name: "allowanceUsage", type: "uint256", internalType: "uint256" }, + { name: "limit", type: "uint256", internalType: "uint256" }, + { name: "period", type: "uint64", internalType: "uint64" }, + ], + }, + { + type: "error", + name: "CallPolicyViolated", + inputs: [ + { name: "target", type: "address", internalType: "address" }, + { name: "selector", type: "bytes4", internalType: "bytes4" }, + ], + }, + { type: "error", name: "CallReverted", inputs: [] }, + { + type: "error", + name: "ConditionFailed", + inputs: [ + { name: "param", type: "bytes32", internalType: "bytes32" }, + { name: "refValue", type: "bytes32", internalType: "bytes32" }, + { name: "condition", type: "uint8", internalType: "uint8" }, + ], + }, + { + type: "error", + name: "InvalidDataLength", + inputs: [ + { name: "actualLength", type: "uint256", internalType: "uint256" }, + { name: "expectedLength", type: "uint256", internalType: "uint256" }, + ], + }, + { + type: "error", + name: "InvalidSignature", + inputs: [ + { name: "msgSender", type: "address", internalType: "address" }, + { name: "thisAddress", type: "address", internalType: "address" }, + ], + }, + { + type: "error", + name: "LifetimeUsageExceeded", + inputs: [ + { name: "lifetimeUsage", type: "uint256", internalType: "uint256" }, + { name: "limit", type: "uint256", internalType: "uint256" }, + ], + }, + { + type: "error", + name: "MaxValueExceeded", + inputs: [ + { name: "value", type: "uint256", internalType: "uint256" }, + { name: "maxValuePerUse", type: "uint256", internalType: "uint256" }, + ], + }, + { type: "error", name: "NoCallsToExecute", inputs: [] }, + { type: "error", name: "SessionExpired", inputs: [] }, + { type: "error", name: "SessionExpiresTooSoon", inputs: [] }, + { type: "error", name: "SessionZeroSigner", inputs: [] }, + { + type: "error", + name: "TransferPolicyViolated", + inputs: [{ name: "target", type: "address", internalType: "address" }], + }, + { type: "error", name: "UIDAlreadyProcessed", inputs: [] }, +] as const;