Skip to content

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}`);