Account Abstracted Instant Actions (Frontends)
This section explains how and why we the ERC-4337 “account abstraction” style login is used for Instant Actions. It will cover:
• What ERC-4337 and EIP-1271 bring to the table
• The end-to-end flow for Instant Login (fetching a nonce, building a SIWE message, hashing/signing, local EIP-1271 validation, HTTP call)
• A complete TypeScript code snippet showing how to call /login
and obtain an access token
Why ERC-4337 / Account Abstraction?
Traditional EOAs (Externally Owned Accounts) sign every transaction with their private key. Smart-contract wallets (Gnosis Safe, Argent, etc.) cannot expose a raw private key.
ERC-4337 let’s a smart-contract wallet “personal_sign
” arbitrary data off-chain, then verify that signature on-chain via the standard EIP-1271 isValidSignature(bytes32, bytes)
call.
Benefits for Instant Actions: • Users grant a single “session” signature once. • Solvers can verify authorization on-chain or locally. • No need for the user to re-sign every open/close quote.
High-Level Login Flow
Fetch a nonce from
/nonce/{subAccount}
Construct a SIWE message (domain, wallet address, statement, nonce, issuedAt, expiration)
Apply EIP-191 prefix + keccak256 → “ERC-4337 hash”
Ask the Safe to personal_sign the raw SIWE string (SigningMethod.ETH_SIGN)
(Optional) Locally verify the signature via
protocolKit.isValidSignature(erc4337Hash, sig)
POST to
/login
with •account_address
•issued_at
•expiration_time
•nonce
•signature
(packed Safe signatures) •sign_type: “ERC4337”
Server validates on-chain via EIP-1271 and returns an access token
Use that token as Authorization: Bearer <access_token> for all subsequent instant open/close calls.
Detailed Code Example (TypeScript)
import Safe, { hashSafeMessage } from '@safe-global/protocol-kit'
import { SigningMethod } from '@safe-global/types-kit'
import { ethers } from 'ethers'
import axios from 'axios'
async function main() {
// ── Configuration ─────────────────────────────────────────
const RPC_URL = 'https://rpc.ankr.com/base/…'
const SAFE_OWNER = '0x…' // your EOA private key
const SAFE_ADDRESS = '0x…' // your Safe contract
const SUBACCOUNT = '0x…' // sub-account for login
const SOLVER_BASE = 'https://base-hedger82.rasa.capital'
const DOMAIN = 'localhost'
const ORIGIN = 'http://localhost:3000'
const CHAIN_ID = 8453
// 1) initialize Safe SDK (this is the account compatible with ERC4337)
const protocolKit = await Safe.init({
provider: RPC_URL,
signer: SAFE_OWNER,
safeAddress: SAFE_ADDRESS
})
// 2) fetch nonce
const nonce = await axios
.get(`${SOLVER_BASE}/nonce/${SUBACCOUNT}`)
.then(r => r.data.nonce)
// 3) build SIWE message string (exactly same style as your code)
const issuedAt = new Date().toISOString()
const expirationTime = new Date(Date.now() + 86_400_000).toISOString()
const siweMessage = `${DOMAIN} wants you to sign in with your Ethereum account:
${SAFE_ADDRESS}
msg: ${SUBACCOUNT}
URI: ${SOLVER_BASE}/login
Version: 1
Chain ID: ${CHAIN_ID}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`
// 4) compute ERC‐4337/EIP-191 hash
function encodeERC4337(msg: string): string {
const hexMsg = Buffer.from(msg, 'utf8').toString('hex')
const prefix = `\x19Ethereum Signed Message:\n${msg.length}`
const prefixHex= Buffer.from(prefix, 'utf8').toString('hex')
return ethers.keccak256('0x' + prefixHex + hexMsg)
}
const erc4337Hash = encodeERC4337(siweMessage)
console.log('[ERC4337] message hash:', erc4337Hash)
// 5) personal_sign via the Safe (EIP-191)
const safeMsg = protocolKit.createMessage(siweMessage)
const signed = await protocolKit.signMessage(
safeMsg,
SigningMethod.ETH_SIGN, // personal_sign / EIP-191
SAFE_ADDRESS
)
const signature = signed.encodedSignatures()
// 6) local EIP-1271 validation (optional)
const ok = await protocolKit.isValidSignature(erc4337Hash, signature)
if (!ok) throw new Error('Local EIP-1271 check failed')
// 7) send the login request
const loginBody = {
account_address: SUBACCOUNT,
issued_at: issuedAt,
expiration_time: expirationTime,
nonce,
signature,
sign_type: 'ERC4337' // tell server “use EIP-1271 flow”
}
const resp = await axios.post(
`${SOLVER_BASE}/login`,
loginBody,
{
headers: {
'Content-Type': 'application/json',
Origin: ORIGIN,
Referer: ORIGIN
}
}
)
console.log('Login response:', resp.data)
// → { access_token: 'eyJ…' }
}
main().catch(console.error)
When the server receives your POST body, the solver backend will:
• Reconstruct the same SIWE message (using your domain, URI, nonce, subaccount, etc.)
• Re-compute the ERC-4337 hash (EIP-191 prefix + keccak256)
• Call userSmartWallet.isValidSignature(hash, signature)
on-chain
– If it returns 0x1626ba7e
, the signature is valid
• If valid, mint you a JWT access_token with an expiry and your sub-account embedded
Using Your Access Token
Once you have access_token
, attach it to every instant-action request:
Authorization: Bearer <access_token>
The solver will trust that token (no further on-chain checks) until it expires.
You can see how to attach this token to requests in the Building an Application with SYMMIO section.
Last updated