How to request ETH from MagicSpend++ protocol
This guide will show you how to request ETH from the MagicSpend++ contracts. You'll be asked to create a stake in the Base Sepolia network and then you can request ETH from the MagicSpend++ protocol on Mainnet Sepolia.
Steps
Define imports and create the clients
import { createSmartAccountClient } from "permissionless"
import { Address, Hex, createPublicClient, encodeFunctionData, getContract, http, parseEther, toHex } from "viem"
import { sepolia } from "viem/chains"
import { privateKeyToAccount } from "viem/accounts"
import { MagicSpendStakeManagerAbi } from "./abi/MagicSpendStakeManager";
import { MagicSpendWithdrawalManagerAbi } from "./abi/MagicSpendWithdrawalManager";
import { entryPoint07Address } from "viem/account-abstraction";
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { toSimpleSmartAccount } from "permissionless/accounts";
import { MagicSpendAllowance, MagicSpendWithdrawal } from "./types";
import "dotenv/config"
const RPC_URL = "https://11155111.rpc.thirdweb.com"
const ETH: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
const amount = 123; // 123 wei
const PRIVATE_KEY = process.env.ACCOUNT_PRIVATE_KEY;
if (PRIVATE_KEY === undefined) {
throw new Error("ACCOUNT_PRIVATE_KEY env var is required")
}
const PIMLICO_URL = process.env.PIMLICO_URL;
if (PIMLICO_URL === undefined) {
throw new Error("PIMLICO_URL env var is required")
}
export const publicClient = createPublicClient({
transport: http(RPC_URL),
chain: sepolia,
})
const pimlicoClient = createPimlicoClient({
transport: http(PIMLICO_URL),
chain: sepolia,
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
})
const sendMagicSpendRequest = async (method: string, params: any[]) => {
const body = {
jsonrpc: "2.0",
method,
params,
id: 1,
};
const response = await fetch(PIMLICO_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const j = await response.json();
// @ts-ignore
return j.result;
}
// This is the address of the account that owns the stake
const signer = privateKeyToAccount(PRIVATE_KEY as Hex)
console.log(`Signer address: ${signer.address}`)
const simpleAccount = await toSimpleSmartAccount({
client: publicClient,
owner: signer,
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
const smartAccountClient = createSmartAccountClient({
account: simpleAccount,
chain: sepolia,
bundlerTransport: http(PIMLICO_URL, {
timeout: 60_000,
}),
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast
},
},
paymaster: pimlicoClient,
})
Fetch MagicSpend contract addresses
const {
stakeManagerAddress,
withdrawalManagerAddress,
} = await sendMagicSpendRequest(
'pimlico_getMagicSpendContracts',
[]
)
const stakeManagerContract = getContract({
abi: MagicSpendStakeManagerAbi,
address: stakeManagerAddress,
client: publicClient,
})
Fetch stakes
const stakes = await sendMagicSpendRequest(
'pimlico_getMagicSpendStakes',
[{
account: signer.address,
asset: ETH
}]
)
if (stakes.length === 0) {
throw new Error("No stakes found")
}
Prepare the Allowance
and sign it
const allowance = (await sendMagicSpendRequest(
'pimlico_prepareMagicSpendAllowance',
[{
account: signer.address,
token: ETH,
amount: toHex(amount),
}]
)) as MagicSpendAllowance;
const hash_ = await stakeManagerContract.read.getAllowanceHash([
{
...allowance,
validAfter: Number(allowance.validAfter),
validUntil: Number(allowance.validUntil),
salt: Number(allowance.salt),
}
]) as Hex;
const allowanceSignature = await signer.signMessage({
message: {
raw: hash_,
}
})
Grant the Allowance
to Pimlico
await sendMagicSpendRequest(
"pimlico_grantMagicSpendAllowance",
[{
allowance,
signature: allowanceSignature
}]
);
Sponsor the Withdrawal
After the Allowance
is granted, you can sponsor the Withdrawal
to request ETH from the MagicSpend++ protocol. Notice how the request details (recipient, amount, etc) are covered with signature. The signature must be done by the Allowance.signer
.
const withdrawalManagerContract = getContract({
abi: MagicSpendWithdrawalManagerAbi,
address: withdrawalManagerAddress,
client: publicClient,
})
const operatorRequestHash = await withdrawalManagerContract.read.getWithdrawalHash([
{
token: ETH,
amount: BigInt(amount),
chainId: BigInt(sepolia.id),
recipient: simpleAccount.address,
preCalls: [],
postCalls: [],
validUntil: Number(0),
validAfter: Number(0),
salt: 0
}
]) as Hex;
const operatorRequestSignature = await signer.signMessage({
message: {
raw: operatorRequestHash
}
})
const [wiithdrawal, withdrawalSignature] = await sendMagicSpendRequest(
"pimlico_sponsorMagicSpendWithdrawal",
[{
recipient: simpleAccount.address,
token: ETH,
amount,
salt: 0,
signature: operatorRequestSignature
}]
)
Execute the Withdrawal
const magicSpendCallData = encodeFunctionData({
abi: MagicSpendWithdrawalManagerAbi,
functionName: 'withdraw',
args: [
wiithdrawal,
withdrawalSignature,
]
})
// Send user operation and withdraw funds
// You can add subsequent calls after the withdrawal, like "buy NFT on OpenSea for ETH"
const userOpHash = await smartAccountClient.sendUserOperation({
account: simpleAccount,
calls: [
{
to: withdrawalManagerAddress,
value: parseEther("0"),
data: magicSpendCallData,
}
]
})
console.log(`Userop hash: ${userOpHash}`);
const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash
})
console.log(`Transaction hash: ${receipt.receipt.transactionHash}`);