Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-pumpkins-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Add ERC4337 extensions
3 changes: 3 additions & 0 deletions packages/thirdweb/src/exports/deploys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ export {
type ERC1155ContractType,
deployERC1155Contract,
} from "../extensions/prebuilts/deploy-erc1155.js";

export { prepareDirectDeployTransaction } from "../contract/deployment/deploy-with-abi.js";
export { deployViaAutoFactory } from "../contract/deployment/deploy-via-autofactory.js";
60 changes: 60 additions & 0 deletions packages/thirdweb/src/exports/extensions/erc4337.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// ACCOUNT
export {
type AddAdminOptions,
addAdmin,
} from "../../extensions/erc4337/account/addAdmin.js";

export {
type RemoveAdminOptions,
removeAdmin,
} from "../../extensions/erc4337/account/removeAdmin.js";

export {
type AddSessionKeyOptions,
addSessionKey,
} from "../../extensions/erc4337/account/addSessionKey.js";

export {
type RemoveSessionKeyOptions,
removeSessionKey,
} from "../../extensions/erc4337/account/removeSessionKey.js";

export { getAllActiveSigners } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllActiveSigners.js";
export { getAllAdmins } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllAdmins.js";
export { getAllSigners } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getAllSigners.js";
export {
getPermissionsForSigner,
type GetPermissionsForSignerParams,
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/getPermissionsForSigner.js";
export {
isActiveSigner,
type IsActiveSignerParams,
} from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js";
export { isAdmin } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isAdmin.js";
export { adminUpdatedEvent } from "../../extensions/erc4337/__generated__/IAccountPermissions/events/AdminUpdated.js";
export { signerPermissionsUpdatedEvent } from "../../extensions/erc4337/__generated__/IAccountPermissions/events/SignerPermissionsUpdated.js";

// FACTORY
export { getAllAccounts } from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAllAccounts.js";
export {
getAccountsOfSigner,
type GetAccountsOfSignerParams,
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAccountsOfSigner.js";
export {
getAddress as predictAccountAddress,
type GetAddressParams as PredictAccountAddressParams,
} from "../../extensions/erc4337/__generated__/IAccountFactory/read/getAddress.js";

// ENTRYPOINT

export { accountDeployedEvent } from "../../extensions/erc4337/__generated__/IEntryPoint/events/AccountDeployed.js";
export { userOperationEventEvent } from "../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationEvent.js";
export { userOperationRevertReasonEvent } from "../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationRevertReason.js";
export {
getUserOpHash,
type GetUserOpHashParams,
} from "../../extensions/erc4337/__generated__/IEntryPoint/read/getUserOpHash.js";
export {
simulateHandleOp,
type SimulateHandleOpParams,
} from "../../extensions/erc4337/__generated__/IEntryPoint/write/simulateHandleOp.js";
50 changes: 50 additions & 0 deletions packages/thirdweb/src/extensions/erc4337/account/addAdmin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { setPermissionsForSigner } from "../__generated__/IAccountPermissions/write/setPermissionsForSigner.js";
import { defaultPermissionsForAdmin, signPermissionRequest } from "./common.js";

export type AddAdminOptions = {
/**
* The admin account that will perform the operation.
*/
account: Account;
/**
* The address to add as an admin.
*/
adminAddress: string;
};

/**
* Adds admin permissions for a specified address.
* @param options - The options for the addAdmin function.
* @returns The transaction object to be sent.
* @example
* ```
* import { addAdmin } from 'thirdweb/extensions/erc4337';
*
* const transaction = addAdmin({
* contract,
* account,
* adminAddress: '0x...'
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC4337
*/
export function addAdmin(options: BaseTransactionOptions<AddAdminOptions>) {
const { contract, account, adminAddress } = options;
return setPermissionsForSigner({
contract,
async asyncParams() {
const { req, signature } = await signPermissionRequest({
account,
contract,
req: await defaultPermissionsForAdmin({
target: adminAddress,
action: "add-admin",
}),
});
return { signature, req };
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { setPermissionsForSigner } from "../__generated__/IAccountPermissions/write/setPermissionsForSigner.js";
import { signPermissionRequest, toContractPermissions } from "./common.js";
import type { AccountPermissions } from "./types.js";

export type AddSessionKeyOptions = {
/**
* The adming account that will perform the operation.
*/
account: Account;
/**
* The address to add as a session key.
*/
sessionKeyAddress: string;
/**
* The permissions to assign to the session key.
*/
permissions: AccountPermissions;
};

/**
* Adds session key permissions for a specified address.
* @param options - The options for the removeSessionKey function.
* @returns The transaction object to be sent.
* @example
* ```
* import { addSessionKey } from 'thirdweb/extensions/erc4337';
*
* const transaction = addSessionKey({
* contract,
* account,
* sessionKeyAddress,
* permissions: {
* approvedTargets: ['0x...'],
* nativeTokenLimitPerTransaction: 0.1, // in ETH
* permissionStartTimestamp: new Date(),
* permissionEndTimestamp: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year from now
* }
* });
* await sendTransaction({ transaction, account });
* ```
* @extension ERC4337
*/
export function addSessionKey(
options: BaseTransactionOptions<AddSessionKeyOptions>,
) {
const { contract, sessionKeyAddress, account, permissions } = options;
return setPermissionsForSigner({
contract,
async asyncParams() {
const { req, signature } = await signPermissionRequest({
account,
contract,
req: await toContractPermissions({
target: sessionKeyAddress,
permissions,
}),
});
return { signature, req };
},
});
}
82 changes: 82 additions & 0 deletions packages/thirdweb/src/extensions/erc4337/account/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ADDRESS_ZERO } from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import { dateToSeconds, tenYearsFromNow } from "../../../utils/date.js";
import { toWei } from "../../../utils/units.js";
import { randomBytes32 } from "../../../utils/uuid.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import type { SetPermissionsForSignerParams } from "../__generated__/IAccountPermissions/write/setPermissionsForSigner.js";
import { SignerPermissionRequest, type AccountPermissions } from "./types.js";

/**
* @internal
*/
export async function signPermissionRequest(options: {
account: Account;
contract: ThirdwebContract;
req: SetPermissionsForSignerParams["req"];
}) {
const { account, contract, req } = options;
const signature = await account.signTypedData({
domain: {
name: "Account",
version: "1",
verifyingContract: contract.address,
chainId: contract.chain.id,
},
primaryType: "SignerPermissionRequest",
types: { SignerPermissionRequest },
message: req,
});
return { req, signature };
}

/**
* @internal
*/
export async function toContractPermissions(options: {
target: string;
permissions: AccountPermissions;
}): Promise<SetPermissionsForSignerParams["req"]> {
const { target, permissions } = options;
return {
approvedTargets:
permissions.approvedTargets === "*"
? [ADDRESS_ZERO]
: permissions.approvedTargets,
nativeTokenLimitPerTransaction: toWei(
permissions.nativeTokenLimitPerTransaction?.toString() || "0",
),
permissionStartTimestamp: dateToSeconds(
permissions.permissionStartTimestamp || new Date(0),
),
permissionEndTimestamp: dateToSeconds(
permissions.permissionEndTimestamp || tenYearsFromNow(),
),
reqValidityStartTimestamp: 0n,
reqValidityEndTimestamp: dateToSeconds(tenYearsFromNow()),
uid: await randomBytes32(),
isAdmin: 0, // session key flag
signer: target,
};
}

/**
* @internal
*/
export async function defaultPermissionsForAdmin(options: {
target: string;
action: "add-admin" | "remove-admin";
}): Promise<SetPermissionsForSignerParams["req"]> {
const { target, action } = options;
return {
approvedTargets: [],
nativeTokenLimitPerTransaction: 0n,
permissionStartTimestamp: 0n,
permissionEndTimestamp: 0n,
reqValidityStartTimestamp: 0n,
reqValidityEndTimestamp: dateToSeconds(tenYearsFromNow()),
uid: await randomBytes32(),
isAdmin: action === "add-admin" ? 1 : action === "remove-admin" ? 2 : 0,
signer: target,
};
}
Loading