Fetch minimal account address from bundler (#7919)
``` <!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Replaces the hardcoded `MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS` with a dynamic fetch from the bundler using the `tw_getDelegationContract` RPC method. This change: - Introduces an async function `getDelegationContractAddress` to fetch the address. - Implements caching for the fetched address to prevent redundant RPC calls. - Updates `_sendTxWithAuthorization` and `is7702MinimalAccount` to handle the async nature and use the dynamically fetched address. - Ensures type safety for address fields. ## How to test Unit tests cover the functionality. A temporary test was created and verified during development. ``` pnpm test packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts ``` --> ``` --- [Slack Thread](https://thirdwebdev.slack.com/archives/C085FEPFLN9/p1756220164604699?thread_ts=1756220164.604699&cid=C085FEPFLN9) <a href="https://cursor.com/background-agent?bcId=bc-874d9753-17ac-4429-8dfb-c3d6776f6711"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"> <img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"> </picture> </a> <a href="https://cursor.com/agents?id=bc-874d9753-17ac-4429-8dfb-c3d6776f6711"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"> <img alt="Open in Web" src="https://cursor.com/open-in-web.svg"> </picture> </a> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on updating the `erc7702` extension, refining event filters, and modifying function signatures to enhance clarity and correctness. It also introduces delegation contract handling in the `minimal-account` implementation. ### Detailed summary - Updated function signatures and event filters in multiple files to replace `signer` with `user` and `newSigner`. - Improved data structures in the generated files for `MinimalAccount`. - Added delegation contract address fetching in `minimal-account.ts`. - Adjusted transaction handling logic to use the delegation contract address. - Enhanced error handling for RPC responses. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Minimal account updated: token-receiver callbacks, interface support check, payable receive, new executeWithSig entrypoint, and a ValueReceived event; SessionCreated/Executed events now include user/newSigner. - Bug Fixes - Delegation contract resolved dynamically via bundler with caching and improved error handling. - Address normalization for transactions and nonce lookups to reduce mismatch errors. - Removed a redundant error from the ABI. - Tests - Tests updated to assert renamed event fields (user/newSigner). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
[
|
||||
"error AllowanceExceeded(uint256 allowanceUsage, uint256 limit, uint64 period)",
|
||||
"error CallPolicyViolated(address target, bytes4 selector)",
|
||||
"error CallReverted()",
|
||||
"error ConditionFailed(bytes32 param, bytes32 refValue, uint8 condition)",
|
||||
"error InvalidDataLength(uint256 actualLength, uint256 expectedLength)",
|
||||
"error InvalidSignature(address msgSender, address thisAddress)",
|
||||
@@ -13,8 +12,9 @@
|
||||
"error SessionZeroSigner()",
|
||||
"error TransferPolicyViolated(address target)",
|
||||
"error UIDAlreadyProcessed()",
|
||||
"event Executed(address indexed to, uint256 value, bytes data)",
|
||||
"event SessionCreated(address indexed signer, (address signer, bool isWildcard, uint256 expiresAt, (address target, bytes4 selector, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit, (uint8 condition, uint64 index, bytes32 refValue, (uint8 limitType, uint256 limit, uint256 period) limit)[] constraints)[] callPolicies, (address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[] transferPolicies, bytes32 uid) sessionSpec)",
|
||||
"event Executed(address indexed user, address indexed signer, address indexed executor, uint256 batchSize)",
|
||||
"event SessionCreated(address indexed user, address indexed newSigner, (address signer, bool isWildcard, uint256 expiresAt, (address target, bytes4 selector, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit, (uint8 condition, uint64 index, bytes32 refValue, (uint8 limitType, uint256 limit, uint256 period) limit)[] constraints)[] callPolicies, (address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[] transferPolicies, bytes32 uid) sessionSpec)",
|
||||
"event ValueReceived(address indexed user, address indexed from, uint256 value)",
|
||||
"function createSessionWithSig((address signer, bool isWildcard, uint256 expiresAt, (address target, bytes4 selector, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit, (uint8 condition, uint64 index, bytes32 refValue, (uint8 limitType, uint256 limit, uint256 period) limit)[] constraints)[] callPolicies, (address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[] transferPolicies, bytes32 uid) sessionSpec, bytes signature)",
|
||||
"function eip712Domain() view returns (bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions)",
|
||||
"function execute((address target, uint256 value, bytes data)[] calls) payable",
|
||||
@@ -23,5 +23,10 @@
|
||||
"function getSessionExpirationForSigner(address signer) view returns (uint256)",
|
||||
"function getSessionStateForSigner(address signer) view returns (((uint256 remaining, address target, bytes4 selector, uint256 index)[] transferValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callParams))",
|
||||
"function getTransferPoliciesForSigner(address signer) view returns ((address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[])",
|
||||
"function isWildcardSigner(address signer) view returns (bool)"
|
||||
"function isWildcardSigner(address signer) view returns (bool)",
|
||||
"function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) returns (bytes4)",
|
||||
"function onERC1155Received(address, address, uint256, uint256, bytes) returns (bytes4)",
|
||||
"function onERC721Received(address, address, uint256, bytes) returns (bytes4)",
|
||||
"function supportsInterface(bytes4 interfaceId) view returns (bool)",
|
||||
"receive() external payable"
|
||||
]
|
||||
+17
-4
@@ -5,9 +5,19 @@ import { prepareEvent } from "../../../../../event/prepare-event.js";
|
||||
* Represents the filters for the "Executed" event.
|
||||
*/
|
||||
export type ExecutedEventFilters = Partial<{
|
||||
to: AbiParameterToPrimitiveType<{
|
||||
user: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "to";
|
||||
name: "user";
|
||||
indexed: true;
|
||||
}>;
|
||||
signer: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "signer";
|
||||
indexed: true;
|
||||
}>;
|
||||
executor: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "executor";
|
||||
indexed: true;
|
||||
}>;
|
||||
}>;
|
||||
@@ -26,7 +36,9 @@ export type ExecutedEventFilters = Partial<{
|
||||
* contract,
|
||||
* events: [
|
||||
* executedEvent({
|
||||
* to: ...,
|
||||
* user: ...,
|
||||
* signer: ...,
|
||||
* executor: ...,
|
||||
* })
|
||||
* ],
|
||||
* });
|
||||
@@ -34,7 +46,8 @@ export type ExecutedEventFilters = Partial<{
|
||||
*/
|
||||
export function executedEvent(filters: ExecutedEventFilters = {}) {
|
||||
return prepareEvent({
|
||||
signature:
|
||||
"event Executed(address indexed user, address indexed signer, address indexed executor, uint256 batchSize)",
|
||||
filters,
|
||||
signature: "event Executed(address indexed to, uint256 value, bytes data)",
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+11
-5
@@ -5,9 +5,14 @@ import { prepareEvent } from "../../../../../event/prepare-event.js";
|
||||
* Represents the filters for the "SessionCreated" event.
|
||||
*/
|
||||
export type SessionCreatedEventFilters = Partial<{
|
||||
signer: AbiParameterToPrimitiveType<{
|
||||
user: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "signer";
|
||||
name: "user";
|
||||
indexed: true;
|
||||
}>;
|
||||
newSigner: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "newSigner";
|
||||
indexed: true;
|
||||
}>;
|
||||
}>;
|
||||
@@ -26,7 +31,8 @@ export type SessionCreatedEventFilters = Partial<{
|
||||
* contract,
|
||||
* events: [
|
||||
* sessionCreatedEvent({
|
||||
* signer: ...,
|
||||
* user: ...,
|
||||
* newSigner: ...,
|
||||
* })
|
||||
* ],
|
||||
* });
|
||||
@@ -34,8 +40,8 @@ export type SessionCreatedEventFilters = Partial<{
|
||||
*/
|
||||
export function sessionCreatedEvent(filters: SessionCreatedEventFilters = {}) {
|
||||
return prepareEvent({
|
||||
filters,
|
||||
signature:
|
||||
"event SessionCreated(address indexed signer, (address signer, bool isWildcard, uint256 expiresAt, (address target, bytes4 selector, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit, (uint8 condition, uint64 index, bytes32 refValue, (uint8 limitType, uint256 limit, uint256 period) limit)[] constraints)[] callPolicies, (address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[] transferPolicies, bytes32 uid) sessionSpec)",
|
||||
"event SessionCreated(address indexed user, address indexed newSigner, (address signer, bool isWildcard, uint256 expiresAt, (address target, bytes4 selector, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit, (uint8 condition, uint64 index, bytes32 refValue, (uint8 limitType, uint256 limit, uint256 period) limit)[] constraints)[] callPolicies, (address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[] transferPolicies, bytes32 uid) sessionSpec)",
|
||||
filters,
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+47
@@ -0,0 +1,47 @@
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
import { prepareEvent } from "../../../../../event/prepare-event.js";
|
||||
|
||||
/**
|
||||
* Represents the filters for the "ValueReceived" event.
|
||||
*/
|
||||
export type ValueReceivedEventFilters = Partial<{
|
||||
user: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "user";
|
||||
indexed: true;
|
||||
}>;
|
||||
from: AbiParameterToPrimitiveType<{
|
||||
type: "address";
|
||||
name: "from";
|
||||
indexed: true;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Creates an event object for the ValueReceived event.
|
||||
* @param filters - Optional filters to apply to the event.
|
||||
* @returns The prepared event object.
|
||||
* @extension ERC7702
|
||||
* @example
|
||||
* ```ts
|
||||
* import { getContractEvents } from "thirdweb";
|
||||
* import { valueReceivedEvent } from "thirdweb/extensions/erc7702";
|
||||
*
|
||||
* const events = await getContractEvents({
|
||||
* contract,
|
||||
* events: [
|
||||
* valueReceivedEvent({
|
||||
* user: ...,
|
||||
* from: ...,
|
||||
* })
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function valueReceivedEvent(filters: ValueReceivedEventFilters = {}) {
|
||||
return prepareEvent({
|
||||
signature:
|
||||
"event ValueReceived(address indexed user, address indexed from, uint256 value)",
|
||||
filters,
|
||||
});
|
||||
}
|
||||
Generated
+7
-7
@@ -8,32 +8,32 @@ export const FN_SELECTOR = "0x84b0196e" as const;
|
||||
const FN_INPUTS = [] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
name: "fields",
|
||||
type: "bytes1",
|
||||
name: "fields",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "name",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "version",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
name: "chainId",
|
||||
type: "uint256",
|
||||
name: "chainId",
|
||||
},
|
||||
{
|
||||
name: "verifyingContract",
|
||||
type: "address",
|
||||
name: "verifyingContract",
|
||||
},
|
||||
{
|
||||
name: "salt",
|
||||
type: "bytes32",
|
||||
name: "salt",
|
||||
},
|
||||
{
|
||||
name: "extensions",
|
||||
type: "uint256[]",
|
||||
name: "extensions",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
+20
-20
@@ -16,81 +16,81 @@ export type GetCallPoliciesForSignerParams = {
|
||||
export const FN_SELECTOR = "0x7103acbb" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "tuple[]",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
type: "bytes4",
|
||||
name: "selector",
|
||||
},
|
||||
{
|
||||
name: "maxValuePerUse",
|
||||
type: "uint256",
|
||||
name: "maxValuePerUse",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "valueLimit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "valueLimit",
|
||||
type: "tuple",
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "constraints",
|
||||
components: [
|
||||
{
|
||||
name: "condition",
|
||||
type: "uint8",
|
||||
name: "condition",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint64",
|
||||
name: "index",
|
||||
},
|
||||
{
|
||||
name: "refValue",
|
||||
type: "bytes32",
|
||||
name: "refValue",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "limit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "limit",
|
||||
type: "tuple",
|
||||
},
|
||||
],
|
||||
name: "constraints",
|
||||
type: "tuple[]",
|
||||
},
|
||||
],
|
||||
type: "tuple[]",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
+1
-1
@@ -16,8 +16,8 @@ export type GetSessionExpirationForSignerParams = {
|
||||
export const FN_SELECTOR = "0xf0a83adf" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
|
||||
+35
-35
@@ -16,81 +16,81 @@ export type GetSessionStateForSignerParams = {
|
||||
export const FN_SELECTOR = "0x74e25eb2" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "tuple",
|
||||
components: [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
name: "remaining",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
type: "bytes4",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint256",
|
||||
},
|
||||
],
|
||||
type: "tuple[]",
|
||||
name: "transferValue",
|
||||
type: "tuple[]",
|
||||
},
|
||||
{
|
||||
components: [
|
||||
{
|
||||
type: "uint256",
|
||||
name: "remaining",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
type: "bytes4",
|
||||
name: "selector",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint256",
|
||||
name: "index",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "callValue",
|
||||
type: "tuple[]",
|
||||
},
|
||||
{
|
||||
components: [
|
||||
{
|
||||
type: "uint256",
|
||||
name: "remaining",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
type: "bytes4",
|
||||
name: "selector",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint256",
|
||||
name: "index",
|
||||
},
|
||||
],
|
||||
name: "callParams",
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "callParams",
|
||||
components: [
|
||||
{
|
||||
type: "uint256",
|
||||
name: "remaining",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
type: "bytes4",
|
||||
name: "selector",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "index",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
type: "tuple",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
+9
-9
@@ -16,41 +16,41 @@ export type GetTransferPoliciesForSignerParams = {
|
||||
export const FN_SELECTOR = "0xed6ed279" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
type: "tuple[]",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "maxValuePerUse",
|
||||
type: "uint256",
|
||||
name: "maxValuePerUse",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "valueLimit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "valueLimit",
|
||||
type: "tuple",
|
||||
},
|
||||
],
|
||||
type: "tuple[]",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -16,8 +16,8 @@ export type IsWildcardSignerParams = {
|
||||
export const FN_SELECTOR = "0x16c258a7" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
|
||||
Generated
+47
-47
@@ -81,134 +81,134 @@ export type CreateSessionWithSigParams = WithOverrides<{
|
||||
export const FN_SELECTOR = "0xb5051648" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "tuple",
|
||||
name: "sessionSpec",
|
||||
components: [
|
||||
{
|
||||
name: "signer",
|
||||
type: "address",
|
||||
name: "signer",
|
||||
},
|
||||
{
|
||||
name: "isWildcard",
|
||||
type: "bool",
|
||||
name: "isWildcard",
|
||||
},
|
||||
{
|
||||
name: "expiresAt",
|
||||
type: "uint256",
|
||||
name: "expiresAt",
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "callPolicies",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "selector",
|
||||
type: "bytes4",
|
||||
name: "selector",
|
||||
},
|
||||
{
|
||||
name: "maxValuePerUse",
|
||||
type: "uint256",
|
||||
name: "maxValuePerUse",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "valueLimit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "valueLimit",
|
||||
type: "tuple",
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "constraints",
|
||||
components: [
|
||||
{
|
||||
name: "condition",
|
||||
type: "uint8",
|
||||
name: "condition",
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint64",
|
||||
name: "index",
|
||||
},
|
||||
{
|
||||
name: "refValue",
|
||||
type: "bytes32",
|
||||
name: "refValue",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "limit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "limit",
|
||||
type: "tuple",
|
||||
},
|
||||
],
|
||||
name: "constraints",
|
||||
type: "tuple[]",
|
||||
},
|
||||
],
|
||||
name: "callPolicies",
|
||||
type: "tuple[]",
|
||||
},
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "transferPolicies",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "maxValuePerUse",
|
||||
type: "uint256",
|
||||
name: "maxValuePerUse",
|
||||
},
|
||||
{
|
||||
type: "tuple",
|
||||
name: "valueLimit",
|
||||
components: [
|
||||
{
|
||||
name: "limitType",
|
||||
type: "uint8",
|
||||
name: "limitType",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "limit",
|
||||
type: "uint256",
|
||||
},
|
||||
{
|
||||
name: "period",
|
||||
type: "uint256",
|
||||
name: "period",
|
||||
},
|
||||
],
|
||||
name: "valueLimit",
|
||||
type: "tuple",
|
||||
},
|
||||
],
|
||||
name: "transferPolicies",
|
||||
type: "tuple[]",
|
||||
},
|
||||
{
|
||||
name: "uid",
|
||||
type: "bytes32",
|
||||
name: "uid",
|
||||
},
|
||||
],
|
||||
name: "sessionSpec",
|
||||
type: "tuple",
|
||||
},
|
||||
{
|
||||
name: "signature",
|
||||
type: "bytes",
|
||||
name: "signature",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [] as const;
|
||||
@@ -316,23 +316,23 @@ export function createSessionWithSig(
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
contract: options.contract,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [resolvedOptions.sessionSpec, resolvedOptions.signature] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
});
|
||||
}
|
||||
|
||||
+16
-16
@@ -26,22 +26,22 @@ export type ExecuteParams = WithOverrides<{
|
||||
export const FN_SELECTOR = "0x3f707e6b" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "calls",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "uint256",
|
||||
name: "value",
|
||||
},
|
||||
{
|
||||
name: "data",
|
||||
type: "bytes",
|
||||
name: "data",
|
||||
},
|
||||
],
|
||||
name: "calls",
|
||||
type: "tuple[]",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [] as const;
|
||||
@@ -137,23 +137,23 @@ export function execute(
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
contract: options.contract,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [resolvedOptions.calls] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+20
-20
@@ -34,36 +34,36 @@ export type ExecuteWithSigParams = WithOverrides<{
|
||||
export const FN_SELECTOR = "0xba61557d" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "tuple",
|
||||
name: "wrappedCalls",
|
||||
components: [
|
||||
{
|
||||
type: "tuple[]",
|
||||
name: "calls",
|
||||
components: [
|
||||
{
|
||||
name: "target",
|
||||
type: "address",
|
||||
name: "target",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "uint256",
|
||||
name: "value",
|
||||
},
|
||||
{
|
||||
name: "data",
|
||||
type: "bytes",
|
||||
name: "data",
|
||||
},
|
||||
],
|
||||
name: "calls",
|
||||
type: "tuple[]",
|
||||
},
|
||||
{
|
||||
name: "uid",
|
||||
type: "bytes32",
|
||||
name: "uid",
|
||||
},
|
||||
],
|
||||
name: "wrappedCalls",
|
||||
type: "tuple",
|
||||
},
|
||||
{
|
||||
name: "signature",
|
||||
type: "bytes",
|
||||
name: "signature",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [] as const;
|
||||
@@ -167,23 +167,23 @@ export function executeWithSig(
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
contract: options.contract,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [resolvedOptions.wrappedCalls, resolvedOptions.signature] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
events: [sessionCreatedEvent()],
|
||||
logs: receipt.logs,
|
||||
});
|
||||
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
|
||||
expect(logs[0]?.args.newSigner).toBe(TEST_ACCOUNT_A.address);
|
||||
});
|
||||
|
||||
it("should allow adding granular session keys", async () => {
|
||||
@@ -124,9 +124,7 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
events: [sessionCreatedEvent()],
|
||||
logs: receipt.logs,
|
||||
});
|
||||
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
|
||||
expect(logs[0]?.args.sessionSpec.callPolicies).toHaveLength(1);
|
||||
expect(logs[0]?.args.sessionSpec.transferPolicies).toHaveLength(1);
|
||||
expect(logs[0]?.args.newSigner).toBe(TEST_ACCOUNT_A.address);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,6 +14,9 @@ import type { SignedAuthorization } from "../../../../transaction/actions/eip770
|
||||
import { toSerializableTransaction } from "../../../../transaction/actions/to-serializable-transaction.js";
|
||||
import type { SendTransactionResult } from "../../../../transaction/types.js";
|
||||
import { getAddress } from "../../../../utils/address.js";
|
||||
import { getClientFetch } from "../../../../utils/fetch.js";
|
||||
import { stringify } from "../../../../utils/json.js";
|
||||
import { withCache } from "../../../../utils/promise/withCache.js";
|
||||
import { randomBytesHex } from "../../../../utils/random.js";
|
||||
import type {
|
||||
Account,
|
||||
@@ -23,10 +26,75 @@ import {
|
||||
executeWithSignature,
|
||||
getQueuedTransactionHash,
|
||||
} from "../../../smart/lib/bundler.js";
|
||||
import { getDefaultBundlerUrl } from "../../../smart/lib/constants.js";
|
||||
import type { BundlerOptions } from "../../../smart/types.js";
|
||||
|
||||
const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS =
|
||||
"0xD6999651Fc0964B9c6B444307a0ab20534a66560";
|
||||
interface DelegationContractResponse {
|
||||
id: string;
|
||||
jsonrpc: string;
|
||||
result: {
|
||||
delegationContract: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the delegation contract address from the bundler using the tw_getDelegationContract RPC method
|
||||
* @internal
|
||||
*/
|
||||
async function getDelegationContractAddress(args: {
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
bundlerUrl?: string;
|
||||
}): Promise<string> {
|
||||
const { client, chain, bundlerUrl } = args;
|
||||
const url = bundlerUrl ?? getDefaultBundlerUrl(chain);
|
||||
|
||||
// Create a cache key based on the bundler URL to ensure we cache per chain/bundler
|
||||
const cacheKey = `delegation-contract:${url}`;
|
||||
|
||||
return withCache(
|
||||
async () => {
|
||||
const fetchWithHeaders = getClientFetch(client);
|
||||
|
||||
const response = await fetchWithHeaders(url, {
|
||||
useAuthToken: true,
|
||||
body: stringify({
|
||||
id: 1,
|
||||
jsonrpc: "2.0",
|
||||
method: "tw_getDelegationContract",
|
||||
params: [],
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch delegation contract: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result: DelegationContractResponse = await response.json();
|
||||
|
||||
if ((result as any).error) {
|
||||
throw new Error(
|
||||
`Delegation contract RPC error: ${JSON.stringify((result as any).error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result.result?.delegationContract) {
|
||||
throw new Error(
|
||||
"Invalid response: missing delegationContract in result",
|
||||
);
|
||||
}
|
||||
|
||||
return result.result.delegationContract;
|
||||
},
|
||||
{ cacheKey, cacheTime: 24 * 60 * 60 * 1000 }, // cache for 24 hours
|
||||
);
|
||||
}
|
||||
|
||||
export const create7702MinimalAccount = (args: {
|
||||
client: ThirdwebClient;
|
||||
@@ -49,7 +117,14 @@ export const create7702MinimalAccount = (args: {
|
||||
});
|
||||
// check if account has been delegated already
|
||||
let authorization: SignedAuthorization | undefined;
|
||||
const isMinimalAccount = await is7702MinimalAccount(eoaContract);
|
||||
const delegationContractAddress = await getDelegationContractAddress({
|
||||
client,
|
||||
chain,
|
||||
});
|
||||
const isMinimalAccount = await is7702MinimalAccount(
|
||||
eoaContract,
|
||||
delegationContractAddress,
|
||||
);
|
||||
if (!isMinimalAccount) {
|
||||
// if not, sign authorization
|
||||
let nonce = firstTx.nonce
|
||||
@@ -58,12 +133,12 @@ export const create7702MinimalAccount = (args: {
|
||||
await getNonce({
|
||||
client,
|
||||
address: adminAccount.address,
|
||||
chain: getCachedChain(firstTx.chainId),
|
||||
chain,
|
||||
}),
|
||||
);
|
||||
nonce += sponsorGas ? 0n : 1n;
|
||||
const auth = await adminAccount.signAuthorization?.({
|
||||
address: MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS,
|
||||
address: getAddress(delegationContractAddress),
|
||||
chainId: firstTx.chainId,
|
||||
nonce,
|
||||
});
|
||||
@@ -76,7 +151,7 @@ export const create7702MinimalAccount = (args: {
|
||||
// send transaction from executor, needs signature
|
||||
const wrappedCalls = {
|
||||
calls: txs.map((tx) => ({
|
||||
data: tx.data ?? "0x", // will throw if undefined address
|
||||
data: tx.data ?? "0x",
|
||||
target: getAddress(tx.to ?? ""),
|
||||
value: tx.value ?? 0n,
|
||||
})),
|
||||
@@ -131,7 +206,7 @@ export const create7702MinimalAccount = (args: {
|
||||
const executeTx = execute({
|
||||
calls: txs.map((tx) => ({
|
||||
data: tx.data ?? "0x",
|
||||
target: tx.to ?? "",
|
||||
target: getAddress(tx.to ?? ""),
|
||||
value: tx.value ?? 0n,
|
||||
})),
|
||||
contract: eoaContract,
|
||||
@@ -228,7 +303,7 @@ async function getNonce(args: {
|
||||
"../../../../rpc/actions/eth_getTransactionCount.js"
|
||||
).then(({ eth_getTransactionCount }) =>
|
||||
eth_getTransactionCount(rpcRequest, {
|
||||
address,
|
||||
address: getAddress(address),
|
||||
blockTag: "pending",
|
||||
}),
|
||||
);
|
||||
@@ -238,14 +313,14 @@ async function getNonce(args: {
|
||||
async function is7702MinimalAccount(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: TODO properly type tw contract
|
||||
eoaContract: ThirdwebContract<any>,
|
||||
delegationContractAddress: string,
|
||||
): Promise<boolean> {
|
||||
const code = await getBytecode(eoaContract);
|
||||
const isDelegated = code.length > 0 && code.startsWith("0xef0100");
|
||||
const target = `0x${code.slice(8, 48)}`;
|
||||
return (
|
||||
isDelegated &&
|
||||
target.toLowerCase() ===
|
||||
MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS.toLowerCase()
|
||||
target.toLowerCase() === delegationContractAddress.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,7 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
if (!executedLog) {
|
||||
throw new Error("No executed log found");
|
||||
}
|
||||
expect(executedLog.args.to).toBe(account.address);
|
||||
expect(executedLog.args.value).toBe(0n);
|
||||
expect(executedLog.args.user).toBe(account.address);
|
||||
});
|
||||
|
||||
it("should sponsor gas for a 4337 smart account", async () => {
|
||||
|
||||
Reference in New Issue
Block a user