permissionless
Version:
A utility library for working with ERC-4337
712 lines (615 loc) • 26.5 kB
text/typescript
import { http, parseEther, zeroAddress } from "viem"
import {
entryPoint06Address,
entryPoint07Address,
entryPoint08Address
} from "viem/account-abstraction"
import { foundry } from "viem/chains"
import { describe, expect } from "vitest"
import {
erc20Address,
sudoMintTokens,
tokenBalanceOf
} from "../../../../mock-paymaster/helpers/erc20-utils"
import { testWithRpc } from "../../../../permissionless-test/src/testWithRpc"
import {
getCoreSmartAccounts,
getPublicClient
} from "../../../../permissionless-test/src/utils"
import { createSmartAccountClient } from "../../../clients/createSmartAccountClient"
import { createPimlicoClient } from "../../../clients/pimlico"
import { prepareUserOperationForErc20Paymaster } from "./prepareUserOperationForErc20Paymaster"
describe.each(getCoreSmartAccounts())(
"prepareUserOperationForErc20Paymaster $name",
({
getSmartAccountClient,
supportsEntryPointV06,
supportsEntryPointV07,
supportsEntryPointV08,
name
}) => {
testWithRpc.skipIf(!supportsEntryPointV06 || name === "Kernel 0.2.1")(
"prepareUserOperationForErc20Paymaster_v06",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.6"
},
...rpc
})
).account
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint06Address,
version: "0.6"
}
})
const publicClient = getPublicClient(anvilRpc)
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(pimlicoClient)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV07)(
"prepareUserOperationForErc20Paymaster_v07",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint07Address,
version: "0.7"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(pimlicoClient)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV08)(
"prepareUserOperationForErc20Paymaster_v08",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.8"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint08Address,
version: "0.8"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(pimlicoClient)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV07)(
"prepareUserOperationForErc20Paymaster_v07 (balanceOverride enabled)",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint07Address,
version: "0.7"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(
pimlicoClient,
{
balanceOverride: true
}
)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV08)(
"prepareUserOperationForErc20Paymaster_v08 (balanceOverride enabled)",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.8"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint08Address,
version: "0.8"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(
pimlicoClient,
{
balanceOverride: true
}
)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV06 || name === "Kernel 0.2.1")(
"prepareUserOperationForErc20Paymaster_v06",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.6"
},
...rpc
})
).account
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint06Address,
version: "0.6"
}
})
const publicClient = getPublicClient(anvilRpc)
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(pimlicoClient)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
callData: await smartAccountClient.account.encodeCalls([
{
to: zeroAddress,
data: "0x",
value: 0n
}
]),
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV07)(
"prepareUserOperationForErc20Paymaster_v07",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint07Address,
version: "0.7"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(pimlicoClient)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
callData: await smartAccountClient.account.encodeCalls([
{
to: zeroAddress,
data: "0x",
value: 0n
}
]),
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
testWithRpc.skipIf(!supportsEntryPointV07)(
"prepareUserOperationForErc20Paymaster_v07 (balanceOverride enabled)",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
...rpc
})
).account
const publicClient = getPublicClient(anvilRpc)
const pimlicoClient = createPimlicoClient({
transport: http(rpc.paymasterRpc),
entryPoint: {
address: entryPoint07Address,
version: "0.7"
}
})
const smartAccountClient = createSmartAccountClient({
// @ts-ignore
client: getPublicClient(anvilRpc),
account,
paymaster: pimlicoClient,
chain: foundry,
userOperation: {
prepareUserOperation:
prepareUserOperationForErc20Paymaster(
pimlicoClient,
{
balanceOverride: true
}
)
},
bundlerTransport: http(rpc.altoRpc)
})
const INITIAL_TOKEN_BALANCE = parseEther("100")
const INTIAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
sudoMintTokens({
amount: INITIAL_TOKEN_BALANCE,
to: smartAccountClient.account.address,
anvilRpc
})
const opHash = await smartAccountClient.sendUserOperation({
callData: await smartAccountClient.account.encodeCalls([
{
to: zeroAddress,
data: "0x",
value: 0n
}
]),
paymasterContext: {
token: erc20Address
}
})
const receipt =
await smartAccountClient.waitForUserOperationReceipt({
hash: opHash
})
expect(receipt).toBeTruthy()
expect(receipt).toBeTruthy()
expect(receipt.success).toBeTruthy()
const FINAL_TOKEN_BALANCE = await tokenBalanceOf(
smartAccountClient.account.address,
rpc.anvilRpc
)
const FINAL_ETH_BALANCE = await publicClient.getBalance({
address: smartAccountClient.account.address
})
expect(FINAL_TOKEN_BALANCE).toBeLessThan(INITIAL_TOKEN_BALANCE) // Token balance should be deducted
expect(FINAL_ETH_BALANCE).toEqual(INTIAL_ETH_BALANCE) // There should be no ETH balance change
}
)
}
)