# Transaction preview

> Explain a raw EVM transaction, an EIP-712 signature request or a Solana transaction in plain language before anyone signs it, with balance changes, approvals and drainer red flags.

Source: https://docs.layerhop.com/guides/transaction-preview

Wallet drainers work because a confirmation screen shows hex. [`POST /v1/preview`](/api-reference#explain-a-transaction-or-signature-request) turns what a dapp asked a wallet to sign into plain language: what leaves the wallet, what comes back, which permissions it grants, who gets what, and red flags. It never asks for a signature and never sends anything. The same tool runs at [layerhop.com/preview](https://layerhop.com/preview) and [app.layerhop.com/preview](https://app.layerhop.com/preview).

```bash
curl -X POST https://api.layerhop.com/v1/preview \
  -H "content-type: application/json" \
  -d '{
    "kind": "evm_tx",
    "chain": 8453,
    "tx": {
      "from": "0x1111111111111111111111111111111111111111",
      "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "data": "0x095ea7b30000000000000000000000002222222222222222222222222222222222222222ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
    }
  }'
```

```json
{
  "preview": {
    "kind": "evm_tx",
    "chainId": 8453,
    "summary": "Danger: lets 0x2222…2222 spend unlimited USDC from your wallet",
    "risk": "danger",
    "approvals": [{ "token": "0x8335…2913", "symbol": "USDC", "spender": "0x2222…2222", "unlimited": true, "via": "approve", "spenderIsContract": false }],
    "flags": [{ "code": "approval_to_eoa", "level": "danger", "message": "0x2222…2222 is a wallet, not an app's contract. Giving a wallet permission to move your USDC is how most drainers work." }],
    "simulation": { "method": "eth_simulateV1", "ok": true, "gasUsed": "55906" }
  }
}
```

## What you can send

| `kind` | Fields | How it's explained |
|---|---|---|
| `evm_tx` | `chain`, `tx.from`, `tx.to`, `tx.data`, `tx.value` (base units) | Simulated against the latest block, and the calldata decoded |
| `typed_data` | `typedData` (object or JSON string), optional `from`, `chain` | Decoded (a signature can't be simulated) |
| `solana_tx` | `tx` (base64, legacy or v0), optional `from` | `simulateTransaction` with the accounts before and after |

## How EVM transactions are simulated

1. **`eth_simulateV1`** with `traceTransfers`: every ERC-20, ERC-721 and ERC-1155 transfer, plus native-coin transfers as synthetic logs. Most public RPCs serve it (checked on Ethereum, Base and Arbitrum, 2026-10-28).
2. Else **`debug_traceCall`** with the call tracer's logs (native transfers from the call frames; reverted frames are skipped).
3. Else a plain **`eth_call`**: it tells whether the transaction would revert, and the balance changes come from decoding the calldata.

`simulation.method` says which one ran; `simulation.ok: false` with `error` means the transaction would fail right now (it also adds a `simulation_failed` flag). A revert is told apart from a node that can't simulate even when the node reports it as `-32000` / `-32603`. `method: "none"` (no RPC could run it) adds a `simulation_failed` caution too: what the transaction moves is unknown.

The calldata is decoded either way: `approve`, `increaseAllowance`, `setApprovalForAll`, `transfer`, `transferFrom`, `safeTransferFrom` (ERC-721 and ERC-1155), `safeBatchTransferFrom`, EIP-2612 `permit`, Permit2 `approve` / `permit` / `transferFrom`, and the calls inside `multicall` and Multicall `aggregate`.

## Signature requests

A signature can move tokens later without another confirmation, which is why drainers ask for one. Decoded types:

- **EIP-2612 `Permit`** and **DAI-style `Permit`** (`allowed: true` is unlimited and, with `expiry: 0`, never expires).
- **Permit2**: `PermitSingle`, `PermitBatch`, `PermitTransferFrom`, `PermitBatchTransferFrom` and their witness variants.
- **Seaport `OrderComponents`** and **`BulkOrder`** (a tree of orders under one signature, each order checked): what the signer offers and who receives what. `gives_away_assets` flags an order that pays the signer nothing back, only dust (under 1/10,000 of a token), sends most of a payment token to someone else, or returns less of a token than it takes (the fake "listing" NFT drain). Prices aren't known, so an order paid in a worthless token isn't caught.

Anything else is reported as `unknown_function`: only sign it on a site you trust. An unreadable order-like message (Blur, 1inch, 0x and other `…Order` types) says so: treat signing it as handing over everything it offers.

## Red flags

| Code | Level | When |
|---|---|---|
| `approval_to_eoa`, `permit_to_eoa` | danger | The spender is a wallet, not a contract |
| `approval_to_unknown`, `permit_to_unknown` | danger if unlimited, else caution | A contract LayerHop can't name |
| `approve_all_nfts` | danger (unknown) / caution (named) | `setApprovalForAll(true)` |
| `unlimited_approval` | caution | Unlimited, to a named spender |
| `long_permit` | caution | The permission lasts more than a year or never expires |
| `value_to_eoa` | caution, danger with calldata | Native coin sent straight to a wallet |
| `unverified_contract`, `new_contract` | caution (`new_contract` spender: danger) | Source not published / deployed less than 7 days ago (needs `ETHERSCAN_API_KEY` on the deployment) |
| `gives_away_assets` | danger | Assets leave the wallet, nothing comes back, and they go to an address the call doesn't name and LayerHop doesn't recognize (a fake `claim()` spending an old allowance, value paid into an unknown contract; a plain `transfer` you send yourself isn't flagged); a Seaport order as above; on Solana, a signer other than the fee payer losing assets, a token account handed to someone else, or a signer's account assigned to another program |
| `community_reports` | caution, danger from 3 approved reports | An address involved has [community reports](/guides/community-reports) a LayerHop admin approved, on the preview's chain. Unreviewed reports are listed in `community` without a flag; Permit2, named spenders and listed tokens never get one |
| `simulation_failed`, `unknown_function`, `wrong_chain` | caution | The transaction would revert (or couldn't be simulated); we can't read the call (even when the simulation shows what it moved); the signature names another chain |

Named spenders are the approval targets LayerHop's own routes use (Permit2, LI.FI, 0x, Relay, deBridge, Mayan, KyberSwap …) and the major DEX routers: the same list as the approvals page. `risk` is the worst flag's level and `summary` starts with "Danger:" when it's `danger`.

## Solana

`simulateTransaction` runs with `sigVerify: false` and a replaced blockhash, so an unsigned transaction works. Address lookup tables are resolved read-only (up to 100 accounts). For `from` (default: the fee payer) the preview reports SOL changes (network fee included) and token balance changes per mint in `balanceChanges`. For every signer it reports new token delegates (`unlimited_approval` when the delegate can move everything), token accounts whose owner would change and signer accounts assigned to another program (`gives_away_assets`). Other signers' changes are listed in `actions`; without `from`, a signer other than the fee payer that loses assets and gets nothing back is `gives_away_assets` (drainers pay the fee themselves and make the victim a second signer). Send `from` with a multi-signer transaction to see your own wallet's changes.

## Limits

20 previews per minute per API key or IP (`RATE_LIMITED`), and a shared budget for all clients; a preview answers within 25 seconds or fails with `PROVIDER_ERROR`. Identical requests within 30 seconds are served from cache (`x-cache: hit`). A preview is a snapshot of the latest block: the result can change by the time the user signs. Flags catch common drainer patterns, not every scam.

## In the app

- [app.layerhop.com/health](https://app.layerhop.com/health) scores any wallet 0–100 from open approvals, risky tokens held, address-poisoning traps, stuck withdrawals and dust, each with a fix-it link; the share card shows only the score.
- Community reports show up in the recipient check, the token risk badge and the preview page. See [Community reports](/guides/community-reports).
