Skip to content

Commit 81cc417

Browse files
cursoragentfirekeeper
andcommitted
Implement dynamic delegation contract fetching for minimal account
Co-authored-by: firekeeper <[email protected]>
1 parent 34239ea commit 81cc417

File tree

1 file changed

+100
-9
lines changed

1 file changed

+100
-9
lines changed

packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,96 @@ import {
2424
getQueuedTransactionHash,
2525
} from "../../../smart/lib/bundler.js";
2626
import type { BundlerOptions } from "../../../smart/types.js";
27+
import { getDefaultBundlerUrl } from "../../../smart/lib/constants.js";
28+
import { getClientFetch } from "../../../../utils/fetch.js";
29+
import { stringify } from "../../../../utils/json.js";
30+
31+
interface DelegationContractResponse {
32+
id: string;
33+
jsonrpc: string;
34+
result: {
35+
delegationContract: string;
36+
};
37+
}
38+
39+
// Cache for delegation contract address to avoid repeated requests
40+
let cachedDelegationContract: string | null = null;
41+
let cachePromise: Promise<string> | null = null;
42+
43+
/**
44+
* Fetches the delegation contract address from the bundler using the tw_getDelegationContract RPC method
45+
* @internal
46+
*/
47+
async function getDelegationContractAddress(args: {
48+
client: ThirdwebClient;
49+
chain: Chain;
50+
bundlerUrl?: string;
51+
}): Promise<string> {
52+
// Return cached result if available
53+
if (cachedDelegationContract) {
54+
return cachedDelegationContract;
55+
}
56+
57+
// If there's already a request in progress, wait for it
58+
if (cachePromise) {
59+
return cachePromise;
60+
}
61+
62+
// Create the promise and cache it
63+
cachePromise = (async () => {
64+
const { client, chain, bundlerUrl } = args;
65+
const url = bundlerUrl ?? getDefaultBundlerUrl(chain);
66+
const fetchWithHeaders = getClientFetch(client);
67+
68+
const response = await fetchWithHeaders(url, {
69+
useAuthToken: true,
70+
body: stringify({
71+
id: 1,
72+
jsonrpc: "2.0",
73+
method: "tw_getDelegationContract",
74+
params: [],
75+
}),
76+
headers: {
77+
"Content-Type": "application/json",
78+
},
79+
method: "POST",
80+
});
81+
82+
if (!response.ok) {
83+
throw new Error(
84+
`Failed to fetch delegation contract: ${response.status} ${response.statusText}`,
85+
);
86+
}
87+
88+
const result: DelegationContractResponse = await response.json();
89+
90+
if ((result as any).error) {
91+
throw new Error(
92+
`Delegation contract RPC error: ${JSON.stringify((result as any).error)}`,
93+
);
94+
}
95+
96+
if (!result.result?.delegationContract) {
97+
throw new Error(
98+
"Invalid response: missing delegationContract in result",
99+
);
100+
}
101+
102+
// Cache the result
103+
cachedDelegationContract = result.result.delegationContract;
104+
return cachedDelegationContract;
105+
})();
106+
107+
try {
108+
const result = await cachePromise;
109+
return result;
110+
} finally {
111+
// Clear the promise cache after completion (success or failure)
112+
cachePromise = null;
113+
}
114+
}
115+
27116

28-
const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS =
29-
"0xD6999651Fc0964B9c6B444307a0ab20534a66560";
30117

31118
export const create7702MinimalAccount = (args: {
32119
client: ThirdwebClient;
@@ -49,7 +136,11 @@ export const create7702MinimalAccount = (args: {
49136
});
50137
// check if account has been delegated already
51138
let authorization: SignedAuthorization | undefined;
52-
const isMinimalAccount = await is7702MinimalAccount(eoaContract);
139+
const delegationContractAddress = await getDelegationContractAddress({
140+
client,
141+
chain,
142+
});
143+
const isMinimalAccount = await is7702MinimalAccount(eoaContract, delegationContractAddress);
53144
if (!isMinimalAccount) {
54145
// if not, sign authorization
55146
let nonce = firstTx.nonce
@@ -58,12 +149,12 @@ export const create7702MinimalAccount = (args: {
58149
await getNonce({
59150
client,
60151
address: adminAccount.address,
61-
chain: getCachedChain(firstTx.chainId),
152+
chain,
62153
}),
63154
);
64155
nonce += sponsorGas ? 0n : 1n;
65156
const auth = await adminAccount.signAuthorization?.({
66-
address: MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
157+
address: getAddress(delegationContractAddress),
67158
chainId: firstTx.chainId,
68159
nonce,
69160
});
@@ -131,7 +222,7 @@ export const create7702MinimalAccount = (args: {
131222
const executeTx = execute({
132223
calls: txs.map((tx) => ({
133224
data: tx.data ?? "0x",
134-
target: tx.to ?? "",
225+
target: getAddress(tx.to ?? ""),
135226
value: tx.value ?? 0n,
136227
})),
137228
contract: eoaContract,
@@ -228,7 +319,7 @@ async function getNonce(args: {
228319
"../../../../rpc/actions/eth_getTransactionCount.js"
229320
).then(({ eth_getTransactionCount }) =>
230321
eth_getTransactionCount(rpcRequest, {
231-
address,
322+
address: getAddress(address),
232323
blockTag: "pending",
233324
}),
234325
);
@@ -238,14 +329,14 @@ async function getNonce(args: {
238329
async function is7702MinimalAccount(
239330
// biome-ignore lint/suspicious/noExplicitAny: TODO properly type tw contract
240331
eoaContract: ThirdwebContract<any>,
332+
delegationContractAddress: string,
241333
): Promise<boolean> {
242334
const code = await getBytecode(eoaContract);
243335
const isDelegated = code.length > 0 && code.startsWith("0xef0100");
244336
const target = `0x${code.slice(8, 48)}`;
245337
return (
246338
isDelegated &&
247-
target.toLowerCase() ===
248-
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS.toLowerCase()
339+
target.toLowerCase() === delegationContractAddress.toLowerCase()
249340
);
250341
}
251342

0 commit comments

Comments
 (0)