@getclave/permissionless
Version:
A utility library for working with ERC-4337
524 lines (454 loc) • 19.9 kB
text/typescript
import { http, parseEther, zeroAddress } from "viem"
import {
entryPoint06Address,
entryPoint07Address,
entryPoint08Address
} from "viem/account-abstraction"
import { privateKeyToAccount } from "viem/accounts"
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"
const privateKey =
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356"
describe.each(getCoreSmartAccounts())(
"prepareUserOperationForErc20Paymaster $name",
({
getSmartAccountClient,
supportsEntryPointV06,
supportsEntryPointV07,
supportsEntryPointV08,
isEip7702Compliant,
name
}) => {
testWithRpc.skipIf(!supportsEntryPointV06 || name === "Kernel 0.2.1")(
"prepareUserOperationForErc20Paymaster_v06",
async ({ rpc }) => {
const { anvilRpc } = rpc
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.6"
},
privateKey,
...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 privateKeyAccount = privateKeyToAccount(privateKey)
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
privateKey,
...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 authorization = isEip7702Compliant
? await privateKeyAccount.signAuthorization({
address: (smartAccountClient.account as any)
.implementation,
chainId: smartAccountClient.chain.id,
nonce: await publicClient.getTransactionCount({
address: smartAccountClient.account.address
})
})
: undefined
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
},
authorization
})
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 privateKeyAccount = privateKeyToAccount(privateKey)
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.8"
},
privateKey,
...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 authorization = isEip7702Compliant
? await privateKeyAccount.signAuthorization({
address: (smartAccountClient.account as any)
.implementation,
chainId: smartAccountClient.chain.id,
nonce: await publicClient.getTransactionCount({
address: smartAccountClient.account.address
})
})
: undefined
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
},
authorization
})
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 privateKeyAccount = privateKeyToAccount(privateKey)
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.7"
},
privateKey,
...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 authorization = isEip7702Compliant
? await privateKeyAccount.signAuthorization({
address: (smartAccountClient.account as any)
.implementation,
chainId: smartAccountClient.chain.id,
nonce: await publicClient.getTransactionCount({
address: smartAccountClient.account.address
})
})
: undefined
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
},
authorization
})
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 privateKeyAccount = privateKeyToAccount(privateKey)
const account = (
await getSmartAccountClient({
entryPoint: {
version: "0.8"
},
privateKey,
...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 authorization = isEip7702Compliant
? await privateKeyAccount.signAuthorization({
address: (smartAccountClient.account as any)
.implementation,
chainId: smartAccountClient.chain.id,
nonce: await publicClient.getTransactionCount({
address: smartAccountClient.account.address
})
})
: undefined
const opHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: zeroAddress,
data: "0x",
value: 0n
}
],
paymasterContext: {
token: erc20Address
},
authorization
})
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
}
)
}
)