ZeroDev SDK
SDK for ZeroDev, based on Kernel.
Getting started
Follow the instructions below to install the packages.
via yarn
yarn add @alchemy/aa-core @zerodevapp/sdk@alpha viem
via npm
npm i -s @alchemy/aa-core @zerodevapp/sdk@alpha viem
Example Usage to Interact with Kernel Accounts
Basic Usage
import { ECDSAValidator } from "@zerodevapp/sdk@alpha";
import { PrivateKeySigner } from "@alchemy/aa-core";
const owner = PrivateKeySigner.privateKeyToAccountSigner(PRIVATE_KEY);
let ecdsaProvider = await ECDSAProvider.init({
projectId,
owner,
});
const { hash } = await ecdsaProvider.sendUserOperation({
target: "0xTargetAddress",
data: "0xcallData",
value: 0n,
});
const tx = await ecdsaProvider.waitForUserOperationTransaction(
result.hash as Hex
);
Batch Transactions
const { hash } = await ecdsaProvider.sendUserOperation([
{
target: "0xTargetAddress1",
data: "0xcallData1",
value: 0n,
},
{
target: "0xTargetAddress2",
data: "0xcallData2",
value: 0n,
},
]);
Optional params for ValidatorProvider:
Option | Usage | Type | Default |
---|
bundlerProvider | Bundler Provider | "ALCHEMY", "STACKUP", "PIMLICO" | "STACKUP" |
usePaymaster | Use paymaster to send userOps | boolean | true |
opts:paymasterConfig:paymasterProvider | Paymaster Provider | "ALCHEMY", "STACKUP", "PIMLICO" | "STACKUP" |
opts:paymasterConfig:onlySendSponsoredTransaction | Only send sponsored transaction and revert if somehow paymaster fails | boolean | false |
opts:paymasterConfig:policy | Paymaster policy | "TOKEN_PAYMASTER", "VERIFYING_PAYMASTER" | "VERIFYING_PAYMASTER" |
opts:paymasterConfig:gasToken | ERC20 token to use for gas fees in case of "TOKEN_PAYMASTER" | "USDC", "PEPE", "TEST_ERC20" | (Required) |
opts:providerConfig:rpcUrl | Custom RPC URL for the bundler provider | string | "https://v0-6-meta-bundler.onrender.com" |
opts:providerConfig:opts:txMaxRetries | The maximum number of times to try fetching a transaction receipt before giving up | number | 5 |
opts:providerConfig:opts:txRetryIntervalMs | The interval in milliseconds to wait between retries while waiting for tx receipts | number | 2000 |
opts:providerConfig:opts:minPriorityFeePerBid | used when computing the fees for a user operation | bigint | 100_000_000n, Chain-wise defaults |
opts:providerConfig:opts:sendTxMaxRetries | The maximum number of times to try sending a transaction before giving up | number | 3 |
opts:providerConfig:opts:sendTxRetryIntervalMs | The interval in milliseconds to wait between retries while sending a transaction | number | 180000 |
opts:accountConfig:index | Index variable to be used alongwith with owner address and validator data while calculating counterfactual address | number | 1000 |
[TODO] include other options | | | |
Pay gas in ERC20
ZeroDev currently supports:
USDC
PEPE
(mainnet only)DAI
(upcoming)
Just pass the paymasterConfig
to createZeroDevProvider
function while creating the provider.
let ecdsaProvider = await ECDSAProvider.init({
projectId,
owner,
opts: {
paymasterConfig: {
policy: "TOKEN_PAYMASTER",
gasToken: "TEST_ERC20",
},
},
});
Change Kernel Account Owner in ECDSAValidator
const ecdsaProvider = await ECDSAProvider.init({
projectId: "c73037ef-8c0b-48be-a581-1f3d161151d3",
owner,
});
const { hash } = await ecdsaProvider.changeOwner(<NEW_OWNER_ADDRESS>);
Via ethers
Signer
import { Wallet } from "@ethersproject/wallet";
import {
ZeroDevEthersProvider,
convertEthersSignerToAccountSigner,
} from "@zerodevapp/sdk@alpha";
const owner = Wallet.fromMnemonic(OWNER_MNEMONIC);
const provider = await ZeroDevEthersProvider.init("ECDSA", {
projectId,
owner: convertEthersSignerToAccountSigner(owner),
opts: {
paymasterConfig: {
policy: "VERIFYING_PAYMASTER",
},
},
});
const signer = provider.getAccountSigner();
const { hash } = await signer.sendUserOperation({
target: "0xTargetAddress",
data: "0xcallData",
value: 0n,
});
Via viem
using custom
transport which supports EIP-1193 providers
import { createWalletClient, custom } from "viem";
import { polygonMumbai } from "viem/chains";
import {
ECDSAProvider,
convertWalletClientToAccountSigner,
} from "@zerodevapp/sdk@alpha";
const client = createWalletClient({
chain: polygonMumbai,
transport: custom(window.ethereum),
});
let ecdsaProvider = await ECDSAProvider.init({
projectId,
owner: convertWalletClientToAccountSigner(client),
});
const { hash } = await ecdsaProvider.sendUserOperation({
target: "0xTargetAddress",
data: "0xcallData",
value: 0n,
});
import { ECDSAProvider, getRPCProviderOwner } from "@zerodevapp/sdk@alpha";
import { Magic } from "magic-sdk";
const magic = new Magic("MAGIC_API_KEY", {
});
let ecdsaProvider = await ECDSAProvider.init({
projectId,
owner: getRPCProviderOwner(magic.rpcProvider),
});
import { ECDSAProvider, getRPCProviderOwner } from "@zerodevapp/sdk@alpha";
import { Web3Auth } from "@web3auth/modal";
const web3auth = new Web3Auth({
});
await web3auth.initModal();
web3auth.connect();
let ecdsaProvider = await ECDSAProvider.init({
projectId,
owner: getRPCProviderOwner(web3auth.provider),
});
Using validator plugins
Kill Switch Validator
A designated guardian can "turn off" the account and set a new owner.
import { constants } from "@zerodevapp/sdk@alpha";
const ecdsaProvider = await ECDSAProvider.init({
projectId,
owner,
});
let result = await ecdsaProvider.sendUserOperation({
target: "0xADDRESS",
data: "0x",
});
await ecdsaProvider.waitForUserOperationTransaction(result.hash as Hex);
const accountAddress = await ecdsaProvider.getAccount().getAddress();
const selector = getFunctionSelector("toggleKillSwitch()");
const blockerKillSwitchProvider = await KillSwitchProvider.init({
projectId,
owner,
guardian,
delaySeconds: 1000,
opts: {
accountConfig: {
accountAddress,
},
validatorConfig: {
mode: ValidatorMode.plugin,
executor: constants.KILL_SWITCH_ACTION,
selector,
},
},
});
const enableSig = await ecdsaProvider
.getValidator()
.approveExecutor(
accountAddress,
selector,
constants.KILL_SWITCH_ACTION,
0,
0,
blockerKillSwitchProvider.getValidator()
);
blockerKillSwitchProvider.getValidator().setEnableSignature(enableSig);
result = await blockerKillSwitchProvider.sendUserOperation({
target: accountAddress,
data: selector,
});
await blockerKillSwitchProvider.waitForUserOperationTransaction(
result.hash as Hex
);
const sudoModeKillSwitchProvider = await KillSwitchProvider.init({
projectId,
owner,
guardian,
delaySeconds: 0,
opts: {
accountConfig: {
accountAddress,
},
validatorConfig: {
mode: ValidatorMode.sudo,
executor: KILL_SWITCH_ACTION,
selector,
},
},
});
const changeOwnerdata = await ecdsaProvider.getEncodedEnableData(
"0xNEW_OWNER_ADDRESS"
);
let result = await sudoModeKillSwitchProvider.sendUserOperation({
target: accountAddress,
data: changeOwnerdata,
});
await sudoModeKillSwitchProvider.waitForUserOperationTransaction(
result.hash as Hex
);
ERC165 Session Key Validator
const ecdsaProvider = await ECDSAProvider.init({
projectId,
owner,
});
let result = await ecdsaProvider.sendUserOperation({
target: "0xADDRESS",
data: "0x",
});
await ecdsaProvider.waitForUserOperationTransaction(result.hash as Hex);
const accountAddress = await ecdsaProvider.getAccount().getAddress();
const selector = getFunctionSelector(
"transferERC721Action(address, uint256, address)"
);
const erc165SessionKeyProvider = await ERC165SessionKeyProvider.init({
projectId,
owner,
sessionKey,
sessionKeyData: {
selector,
erc165InterfaceId: "0x80ac58cd",
validAfter: 0,
validUntil: 0,
addressOffset: 16,
},
opts: {
accountConfig: {
accountAddress,
},
validatorConfig: {
mode: ValidatorMode.plugin,
executor: constants.TOKEN_ACTION,
selector,
},
},
});
const enableSig = await ecdsaProvider
.getValidator()
.approveExecutor(
accountAddress,
selector,
constants.TOKEN_ACTION,
0,
0,
erc165SessionKeyProvider.getValidator()
);
erc165SessionKeyProvider.getValidator().setEnableSignature(enableSig);
const { hash } = await erc165SessionKeyProvider.sendUserOperation({
target: accountAddress,
data: encodeFunctionData({
abi: TokenActionsAbi,
functionName: "transferERC721Action",
args: ["TOKEN_ADDRESS", "TOKEN_ID", "RECIPIENT_ADDRESS"],
}),
});
Components
Core Components
The primary interfaces are the ZeroDevProvider
, KernelSmartContractAccount
and KernelBaseValidator
The ZeroDevProvider
is an ERC-1193 compliant Provider built on top of Alchemy's SmartAccountProvider
sendUserOperation
-- this takes in target
, callData
, and an optional value
which then constructs a UserOperation (UO), sends it, and returns the hash
of the UO. It handles estimating gas, fetching fee data, (optionally) requesting paymasterAndData, and lastly signing. This is done via a middleware stack that runs in a specific order. The middleware order is getDummyPaymasterData
=> estimateGas
=> getFeeData
=> getPaymasterAndData
. The paymaster fields are set to 0x
by default. They can be changed using provider.withPaymasterMiddleware
.sendTransaction
-- this takes in a traditional Transaction Request object which then gets converted into a UO. Currently, the only data being used from the Transaction Request object is from
, to
, data
and value
. Support for other fields is coming soon.
KernelSmartContractAccount
is Kernel's implementation of BaseSmartContractAccount
. 6 main methods are implemented
getDummySignature
-- this method should return a signature that will not revert
during validation. It does not have to pass validation, just not cause the contract to revert. This is required for gas estimation so that the gas estimate are accurate.encodeExecute
-- this method should return the abi encoded function data for a call to your contract's execute
methodencodeExecuteDelegate
-- this method should return the abi encoded function data for a delegate
call to your contract's execute
methodsignMessage
-- this is used to sign UO HashessignWithEip6492
-- this should return an ERC-191 and EIP-6492 compliant message used to personal_signgetAccountInitCode
-- this should return the init code that will be used to create an account if one does not exist. Usually this is the concatenation of the account's factory address and the abi encoded function data of the account factory's createAccount
method.
The KernelBaseValidator
is a plugin that modify how transactions are validated. It allows for extension and implementation of arbitrary validation logic. It implements 3 main methods:
getAddress
-- this returns the address of the validatorgetOwner
-- this returns the eligible signer's address for the active smart walletgetSignature
-- this method signs the userop hash using signer object and then concats additional params based on validator mode.
Contributing
- clone the repo
- run
yarn
- Make changes to packages
Adding new custom validator plugin
-
Create a new validator class that extends KernelBaseValidator
similar to ECDSAValidator
.
-
Make sure to pass the validatorAddress
of your validator to the KernelBaseValidator
base class.
-
Create a new validator provider that extends ValidatorProvider
similar to ECDSAValidatorProvider
.
-
Use the newly created validator provider as per above examples.
KernelBaseValidator
methods to be implemented in your validator class
signer()
-- this method should return the signer as per your validator's implementation. For example, for Multi-Signature validator, this method should return one of the owner signer which is connected to the multisig wallet contract and currently using the DAPP.getOwner()
-- this method should return the address of the signer. For example, for Multi-Signature validator, this method should return the address of the signer which is connected to the multisig wallet contract and currently using the DAPP.getEnableData()
-- this method should return the bytes data for the enable
method of your validator contract. For example, in ECDSA validator, this method returns owner
address as bytes data. This method is used to enable the validator for the first time while creating the account wallet.encodeEnable(enableData: Hex)
-- this method should return the abi encoded function data for the enable
method of your validator contract. For example, in ECDSA validator, this method returns the abi encoded function data for the enable
method with owner address as bytes param.encodeDisable(disableData: Hex)
-- this method should return the abi encoded function data for the disable
method of your validator contract. For example, in ECDSA validator, this method returns the abi encoded function data for the disable
method with empty bytes param since ECDSA Validator doesn't require any param.signMessage(message: Uint8Array | string | Hex)
-- this method should return the signature of the message using the connected signer.signUserOp(userOp: UserOperationRequest)
-- this method should return the signature of the userOp hash using the connected signer.