<?php
// ----- DEBUG (turn off on production) -----
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/php-error.log');

// ----- Robust autoload resolve -----
$autoloadTried = [];
$autoloadOk = false;
foreach ([
    __DIR__ . '/vendor/autoload.php',
    __DIR__ . '/../vendor/autoload.php',
] as $path) {
    $autoloadTried[] = $path;
    if (is_file($path)) { require $path; $autoloadOk = true; break; }
}
if (!$autoloadOk) {
    http_response_code(500);
    echo "<pre>Composer autoload not found.\nTried:\n- " . implode("\n- ", $autoloadTried) . "\nRun: composer require kornrunner/keccak kornrunner/rlp simplito/elliptic-php</pre>";
    exit;
}

use kornrunner\Keccak;
use kornrunner\RLP\RLP;
use Elliptic\EC;

// ---------- RLP helpers (non-static) ----------
function rlp_encode_items(array $items): string {
    static $rlp = null;
    if ($rlp === null) $rlp = new RLP();
    $hex = $rlp->encode($items);
    $bin = hex2bin($hex);
    if ($bin === false) throw new Exception('RLP encode failed.');
    return $bin;
}
function rlp_decode_hex(string $hex) {
    static $rlp = null;
    if ($rlp === null) $rlp = new RLP();
    return $rlp->decode('0x' . $hex);
}

// ---------- utils ----------
function cleanHex(string $h = null): string {
    if ($h === null) return '';
    $h = trim($h);
    if ($h === '') return '';
    if (str_starts_with($h, '0x') || str_starts_with($h, '0X')) $h = substr($h, 2);
    if (!preg_match('/^[0-9a-fA-F]*$/', $h)) return ''; // invalid chars
    if (strlen($h) % 2 === 1) $h = '0' . $h;
    return strtolower($h);
}
function hex2bin_strict(?string $hex): string {
    $h = cleanHex($hex ?? '');
    return $h === '' ? '' : hex2bin($h);
}
function bin_to_hex0x(?string $bin): string {
    if (!is_string($bin) || $bin === '') return '0x';
    $hex = bin2hex($bin);
    return $hex === '' ? '0x' : '0x' . $hex;
}
function ensureHexPrefixed(?string $hex): string {
    $clean = cleanHex($hex ?? '');
    return $clean === '' ? '0x' : '0x' . $clean;
}
function resolveChainId(?string $chainId, string $rpcUrl): string {
    $candidate = ensureHexPrefixed($chainId);
    if ($candidate !== '0x') return $candidate;
    if ($rpcUrl === '') return $candidate;
    static $chainCache = [];
    if (!array_key_exists($rpcUrl, $chainCache)) {
        try {
            $res = rpcCall($rpcUrl, 'eth_chainId');
            if (is_string($res) && $res !== '') {
                $chainCache[$rpcUrl] = ensureHexPrefixed($res);
            } elseif (is_numeric($res)) {
                $chainCache[$rpcUrl] = ensureHexPrefixed('0x' . dechex((int)$res));
            } else {
                $chainCache[$rpcUrl] = '0x';
            }
        } catch (Throwable $e) {
            $chainCache[$rpcUrl] = '0x';
        }
    }
    return $chainCache[$rpcUrl];
}
function isHexPrefixed(?string $s): bool {
    return is_string($s) && preg_match('/^0x[0-9a-fA-F]*$/', $s);
}
function rpcCall(string $rpcUrl, string $method, array $params = []) {
    $payload = json_encode(['jsonrpc'=>'2.0','id'=>1,'method'=>$method,'params'=>$params]);
    $ch = curl_init($rpcUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_TIMEOUT => 15
    ]);
    $res = curl_exec($ch);
    if ($res === false) throw new Exception('RPC Error: ' . curl_error($ch));
    $data = json_decode($res, true);
    if (!is_array($data)) throw new Exception('Invalid RPC response');
    if (isset($data['error'])) throw new Exception('RPC returned error: ' . json_encode($data['error']));
    return $data['result'] ?? null;
}

// ---------- signing hashes ----------
function buildLegacySigningHash(array $tx, ?int $chainId): string {
    $items = [
        hex2bin_strict($tx['nonce']    ?? '0x'),
        hex2bin_strict($tx['gasPrice'] ?? '0x'),
        hex2bin_strict($tx['gas']      ?? $tx['gasLimit'] ?? '0x'),
        hex2bin_strict($tx['to']       ?? '0x'),
        hex2bin_strict($tx['value']    ?? '0x'),
        hex2bin_strict($tx['input']    ?? $tx['data'] ?? '0x'),
    ];
    if (!is_null($chainId) && $chainId >= 0) {
        // per EIP-155 append chainId, 0, 0
        $chainHex = dechex($chainId);
        if (strlen($chainHex) % 2 === 1) $chainHex = '0' . $chainHex;
        $items[] = hex2bin($chainHex);
        $items[] = '';
        $items[] = '';
    }
    $rlpBin = rlp_encode_items($items);
    return bin2hex(Keccak::hash($rlpBin, 256, true)); // returns hex of hash
}

function buildEip1559SigningHash(array $tx): string {
    // Inputs must be hex strings (0x...) or empty
    $accessList = $tx['accessList'] ?? [];
    $alRlp = [];
    if (is_array($accessList)) {
        foreach ($accessList as $al) {
            $addr = hex2bin_strict($al['address'] ?? $al[0] ?? '0x');
            $keysArr = [];
            $keys = $al['storageKeys'] ?? $al[1] ?? [];
            if (is_array($keys)) foreach ($keys as $k) $keysArr[] = hex2bin_strict($k);
            $alRlp[] = [$addr, $keysArr];
        }
    }
    $payload = [
        hex2bin_strict($tx['chainId'] ?? '0x'),
        hex2bin_strict($tx['nonce'] ?? '0x'),
        hex2bin_strict($tx['maxPriorityFeePerGas'] ?? '0x'),
        hex2bin_strict($tx['maxFeePerGas'] ?? '0x'),
        hex2bin_strict($tx['gas'] ?? $tx['gasLimit'] ?? '0x'),
        hex2bin_strict($tx['to'] ?? '0x'),
        hex2bin_strict($tx['value'] ?? '0x'),
        hex2bin_strict($tx['input'] ?? $tx['data'] ?? '0x'),
        $alRlp
    ];
    // ⬅️ تغییر اصلی: استفاده از helper غیر استاتیک
    $rlpBin = rlp_encode_items($payload);
    $withPrefix = "\x02" . $rlpBin; // typed tx prefix
    return bin2hex(Keccak::hash($withPrefix, 256, true));
}

// ---------- crypto helpers ----------
function recoverPubKeyByRecId(string $msgHashHex, string $rHex, string $sHex, int $recId): string {
    $ec = new EC('secp256k1');
    $r = cleanHex($rHex); $s = cleanHex($sHex);
    $msg = cleanHex($msgHashHex);
    if ($r === '' || $s === '' || $msg === '') throw new Exception('Invalid r, s یا msg hash');
    $sig = ['r'=>$r,'s'=>$s];
    $key = $ec->recoverPubKey($msg, $sig, (int)$recId);
    return '0x' . $key->encode('hex'); // 04....
}
function pubkeyToEthAddress(string $pubUncompressedHex): string {
    $p = preg_replace('/^0x04/i', '', $pubUncompressedHex);
    $hash = Keccak::hash(hex2bin($p), 256, true);
    return '0x' . substr(bin2hex($hash), -40);
}
function isTypedRawTx(string $hexNoPrefix): bool {
    // EIP-2718 typed tx: first byte is type (0x01..0x7f), followed by an RLP
    if ($hexNoPrefix === '' || strlen($hexNoPrefix) < 2) return false;
    $first = hexdec(substr($hexNoPrefix, 0, 2));
    return $first >= 0x01 && $first <= 0x7f;
}

// wrapper to keep your calls unchanged
function decodeRlp(string $hex) {
    $hex = trim($hex);
    if ($hex === '') throw new Exception('دادهٔ RLP خالی است.');
    if (str_starts_with($hex, '0x') || str_starts_with($hex, '0X')) $hex = substr($hex, 2);
    if ($hex === '') throw new Exception('دادهٔ RLP خالی است.');
    if (!preg_match('/^[0-9a-fA-F]+$/', $hex)) throw new Exception('دادهٔ RLP شامل کاراکتر نامعتبر است.');
    if (strlen($hex) % 2 === 1) throw new Exception('طول دادهٔ RLP باید زوج باشد.');
    return rlp_decode_to_bin(rlp_decode_hex(strtolower($hex)));
}

function rlp_decode_to_bin($decoded) {
    if (is_array($decoded)) {
        $out = [];
        foreach ($decoded as $k => $v) {
            $out[$k] = rlp_decode_to_bin($v);
        }
        return $out;
    }
    $decoded = (string)$decoded;
    if ($decoded === '') return '';
    $bin = hex2bin($decoded);
    if ($bin === false) throw new Exception('RLP decode segment invalid.');
    return $bin;
}

function recIdCandidatesFromV(int $vInt): array {
    $base = null;
    if ($vInt === 27 || $vInt === 28) {
        $base = $vInt - 27;
    } elseif ($vInt >= 35) {
        $base = ($vInt - 35) % 2;
    } else {
        $base = $vInt % 2;
    }
    $candidates = [];
    $push = function(int $rec) use (&$candidates) {
        if ($rec < 0 || $rec > 3) return;
        if (!in_array($rec, $candidates, true)) $candidates[] = $rec;
    };
    $push($base);
    $push($base ^ 1);
    if ($base < 2) {
        $push($base + 2);
        $push(($base ^ 1) + 2);
    }
    return $candidates;
}

function recoverSignatureData(string $msgHashHex, string $rHex, string $sHex, int $vInt, ?string $expectedAddress = null): array {
    $msg = cleanHex($msgHashHex);
    $r = cleanHex($rHex);
    $s = cleanHex($sHex);
    if ($msg === '' || $r === '' || $s === '') {
        throw new Exception('امضای ورودی نامعتبر است.');
    }
    $expected = $expectedAddress ? strtolower($expectedAddress) : null;
    $results = [];
    foreach (recIdCandidatesFromV($vInt) as $recId) {
        try {
            $pub = recoverPubKeyByRecId($msg, $r, $s, $recId);
            $addr = pubkeyToEthAddress($pub);
            $result = ['pub'=>$pub,'address'=>$addr,'recId'=>$recId];
            $results[] = $result;
            if ($expected !== null && strtolower($addr) === $expected) {
                return ['chosen'=>$result,'candidates'=>$results];
            }
        } catch (Throwable $e) {
            // ignore bad candidate
        }
    }
    if (!$results) throw new Exception('بازیابی کلید ممکن نشد.');
    return ['chosen'=>$results[0],'candidates'=>$results];
}

function analyzeRawTx(string $rawClean, ?string $expectedAddress = null): array {
    if ($rawClean === '') throw new Exception('raw tx نامعتبر است.');
    if (!isTypedRawTx($rawClean)) {
        $decoded = decodeRlp($rawClean);
        if (!is_array($decoded) || count($decoded) < 9) throw new Exception('Legacy raw tx decode failed.');
        [$nonce,$gasPrice,$gasLimit,$to,$value,$data,$vBin,$rBin,$sBin] = $decoded + [null,null,null,null,null,null,null,null,null];
        $v = $vBin === '' || $vBin === null ? null : hexdec(bin2hex($vBin));
        $r = $rBin ? ('0x' . bin2hex($rBin)) : '';
        $s = $sBin ? ('0x' . bin2hex($sBin)) : '';

        $chainId = null;
        if ($v !== null && $v > 28) {
            $cid = intdiv(($v - 35), 2);
            if ($cid > 0) $chainId = $cid;
        }

        $txForBuild = [
            'nonce'    => $nonce ? bin_to_hex0x($nonce) : '0x',
            'gasPrice' => $gasPrice ? bin_to_hex0x($gasPrice) : '0x',
            'gasLimit' => $gasLimit ? bin_to_hex0x($gasLimit) : '0x',
            'to'       => $to ? bin_to_hex0x($to) : '0x',
            'value'    => $value ? bin_to_hex0x($value) : '0x',
            'input'    => $data ? bin_to_hex0x($data) : '0x',
        ];

        $msgHashHex = buildLegacySigningHash($txForBuild, $chainId);
        $rec = recoverSignatureData($msgHashHex, $r, $s, (int)($v ?? 0), $expectedAddress);
        return [
            'type' => 'legacy',
            'pub' => $rec['chosen']['pub'],
            'address' => $rec['chosen']['address'],
            'v' => $v,
            'r' => $r,
            's' => $s,
            'chainId' => $chainId,
            'recId' => $rec['chosen']['recId'],
            'alt_candidates' => $rec['candidates'],
            'tx_from' => $expectedAddress,
        ];
    }

    $type = hexdec(substr($rawClean, 0, 2));
    $payloadHex = substr($rawClean, 2);
    $dec = decodeRlp($payloadHex);

    if ($type === 2) {
        if (!is_array($dec) || count($dec) < 12) throw new Exception('type 0x02 decode failed.');
        $chainId = $dec[0] ?? '';
        $nonce   = $dec[1] ?? '';
        $maxPrio = $dec[2] ?? '';
        $maxFee  = $dec[3] ?? '';
        $gas     = $dec[4] ?? '';
        $to      = $dec[5] ?? '';
        $value   = $dec[6] ?? '';
        $data    = $dec[7] ?? '';
        $alist   = $dec[8] ?? [];
        $vBin    = $dec[9] ?? '';
        $rBin    = $dec[10] ?? '';
        $sBin    = $dec[11] ?? '';

        $v = $vBin === '' || $vBin === null ? 0 : hexdec(bin2hex($vBin));
        $r = $rBin ? ('0x' . bin2hex($rBin)) : '';
        $s = $sBin ? ('0x' . bin2hex($sBin)) : '';

        $txForBuild = [
            'chainId' => bin_to_hex0x($chainId),
            'nonce'   => bin_to_hex0x($nonce),
            'maxPriorityFeePerGas' => bin_to_hex0x($maxPrio),
            'maxFeePerGas' => bin_to_hex0x($maxFee),
            'gas'     => bin_to_hex0x($gas),
            'to'      => bin_to_hex0x($to),
            'value'   => bin_to_hex0x($value),
            'input'   => bin_to_hex0x($data),
            'accessList' => is_array($alist)? array_map(function($el){
                $addr = is_array($el) && array_key_exists(0, $el) ? bin_to_hex0x($el[0]) : '0x';
                $keys = [];
                if (is_array($el) && array_key_exists(1, $el) && is_array($el[1])) {
                    foreach ($el[1] as $k) $keys[] = bin_to_hex0x($k);
                }
                return [$addr, $keys];
            }, $alist) : []
        ];

        $msgHashHex = buildEip1559SigningHash($txForBuild);
        $rec = recoverSignatureData($msgHashHex, $r, $s, (int)$v, $expectedAddress);

        return [
            'type' => 2,
            'pub' => $rec['chosen']['pub'],
            'address' => $rec['chosen']['address'],
            'v' => $v,
            'r' => $r,
            's' => $s,
            'chainId' => $txForBuild['chainId'],
            'recId' => $rec['chosen']['recId'],
            'alt_candidates' => $rec['candidates'],
            'tx_from' => $expectedAddress,
        ];
    }

    throw new Exception("Typed tx type 0x" . dechex($type) . " پشتیبانی نشده.");
}

// ---------- controller ----------
$err = null; $out = null;

try {
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $rpcUrl = trim($_POST['rpc_url'] ?? '');
        $rawTx  = trim($_POST['raw_tx'] ?? '');
        $txHash = trim($_POST['tx_hash'] ?? '');

        if ($rawTx === '' && $txHash === '') throw new Exception('یا raw signed transaction وارد کن یا tx hash.');

        if ($rawTx !== '') {
            $rawClean = cleanHex($rawTx);
            $out = analyzeRawTx($rawClean, null);
        } else {
            // via tx hash + RPC
            if ($rpcUrl === '') throw new Exception('برای tx hash باید RPC URL بدهی.');
            if (!isHexPrefixed($txHash)) throw new Exception('tx hash باید 0x... باشد.');
            $tx = rpcCall($rpcUrl, 'eth_getTransactionByHash', [$txHash]);
            if (!$tx) throw new Exception('تراکنش پیدا نشد.');
            $rawFromRpc = null;
            try {
                $rawFromRpc = rpcCall($rpcUrl, 'eth_getRawTransactionByHash', [$txHash]);
            } catch (Throwable $ignored) {
                $rawFromRpc = null;
            }
            if (is_string($rawFromRpc)) {
                $rawClean = cleanHex($rawFromRpc);
                $out = analyzeRawTx($rawClean, $tx['from'] ?? null);
                if (isset($tx['from'])) $out['tx_from'] = $tx['from'];
            } else {
                $type = isset($tx['type']) ? hexdec($tx['type']) : 0;
                $v = isset($tx['v']) ? hexdec($tx['v']) : 0;
                $r = $tx['r'] ?? '';
                $s = $tx['s'] ?? '';

                if ($type === 2) {
                    $accessList = $tx['accessList'] ?? [];
                    $accessListFmt = [];
                    if (is_array($accessList)) {
                        foreach ($accessList as $al) {
                            $addr = $al['address'] ?? ($al[0] ?? '0x');
                            $keys = $al['storageKeys'] ?? ($al[1] ?? []);
                            $keys = is_array($keys) ? array_map('ensureHexPrefixed', $keys) : [];
                            $accessListFmt[] = [ensureHexPrefixed($addr), $keys];
                        }
                    }
                    $txForBuild = [
                        'chainId' => resolveChainId($tx['chainId'] ?? null, $rpcUrl),
                        'nonce'   => ensureHexPrefixed($tx['nonce'] ?? null),
                        'maxPriorityFeePerGas' => ensureHexPrefixed($tx['maxPriorityFeePerGas'] ?? null),
                        'maxFeePerGas' => ensureHexPrefixed($tx['maxFeePerGas'] ?? null),
                        'gas'     => ensureHexPrefixed($tx['gas'] ?? $tx['gasLimit'] ?? null),
                        'to'      => ensureHexPrefixed($tx['to'] ?? null),
                        'value'   => ensureHexPrefixed($tx['value'] ?? null),
                        'input'   => ensureHexPrefixed($tx['input'] ?? $tx['data'] ?? null),
                        'accessList' => $accessListFmt
                    ];
                    $msgHashHex = buildEip1559SigningHash($txForBuild);
                    $rec = recoverSignatureData($msgHashHex, $r, $s, (int)$v, $tx['from'] ?? null);
                    $pub = $rec['chosen']['pub'];
                    $addr = $rec['chosen']['address'];
                    $out = [
                        'type'=>2,
                        'pub'=>$pub,
                        'address'=>$addr,
                        'v'=>$v,
                        'r'=>$r,
                        's'=>$s,
                        'tx_from'=>$tx['from'] ?? null,
                        'chainId'=>$txForBuild['chainId'],
                        'recId'=>$rec['chosen']['recId'],
                        'alt_candidates'=>$rec['candidates']
                    ];
                } else {
                    $chainId = null;
                    if ($v > 28) {
                        $cid = intdiv(($v - 35), 2);
                        if ($cid > 0) $chainId = $cid;
                    }
                    $txForBuild = [
                        'nonce'    => ensureHexPrefixed($tx['nonce'] ?? null),
                        'gasPrice' => ensureHexPrefixed($tx['gasPrice'] ?? null),
                        'gas'      => ensureHexPrefixed($tx['gas'] ?? $tx['gasLimit'] ?? null),
                        'to'       => ensureHexPrefixed($tx['to'] ?? null),
                        'value'    => ensureHexPrefixed($tx['value'] ?? null),
                        'input'    => ensureHexPrefixed($tx['input'] ?? $tx['data'] ?? null),
                    ];
                    $msgHashHex = buildLegacySigningHash($txForBuild, $chainId);
                    $rec = recoverSignatureData($msgHashHex, $r, $s, (int)$v, $tx['from'] ?? null);
                    $pub = $rec['chosen']['pub'];
                    $addr = $rec['chosen']['address'];
                    $out = [
                        'type'=>'legacy',
                        'pub'=>$pub,
                        'address'=>$addr,
                        'v'=>$v,
                        'r'=>$r,
                        's'=>$s,
                        'chainId'=>$chainId,
                        'tx_from'=>$tx['from'] ?? null,
                        'recId'=>$rec['chosen']['recId'],
                        'alt_candidates'=>$rec['candidates']
                    ];
                }
            }
        }
    }
} catch (Throwable $e) {
    $err = $e->getMessage();
}
?>
<!doctype html>
<html lang="fa"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>بازیابی کلید عمومی از تراکنش (EVM)</title>
<style>
body{font-family:tahoma,arial;background:#f6f8fb;margin:0;padding:24px}
.container{max-width:950px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.06);padding:24px}
label{display:block;margin-top:12px;font-weight:bold}
textarea,input{width:100%;padding:10px;margin-top:6px;border:1px solid #e5e7eb;border-radius:8px}
button{margin-top:14px;padding:10px 16px;background:#2563eb;color:#fff;border:0;border-radius:8px;cursor:pointer}
.result{background:#eef6ff;border-left:4px solid #3b82f6;padding:14px;border-radius:8px;margin-top:16px}
.error{background:#fff2f0;border-left:4px solid #ef4444;padding:14px;border-radius:8px;margin-top:16px}
small{color:#6b7280}
</style></head>
<body>
<div class="container">
  <h2>ریکاوری کلید عمومی از تراکنش‌های EVM (Raw یا TxHash)</h2>
  <p>یا <b>raw signed tx</b> بده یا <b>tx hash</b> + آدرس <b>RPC</b>. از Legacy/EIP-155 و EIP-1559 (type 2) پشتیبانی می‌شود.</p>

  <?php if ($err): ?>
    <div class="error"><b>خطا:</b> <?=htmlspecialchars($err)?></div>
  <?php endif; ?>

  <form method="post">
    <label>Raw signed transaction (hex)</label>
    <textarea name="raw_tx" rows="6" placeholder="0x..."><?=htmlspecialchars($_POST['raw_tx'] ?? '')?></textarea>

    <label>یا Transaction Hash (0x..)</label>
    <input type="text" name="tx_hash" value="<?=htmlspecialchars($_POST['tx_hash'] ?? '')?>" placeholder="0x...">

    <label>RPC URL (برای حالت tx hash)</label>
    <input type="text" name="rpc_url" value="<?=htmlspecialchars($_POST['rpc_url'] ?? '')?>" placeholder="https://mainnet.infura.io/v3/XXXX">

    <small>نکته: اگر هر دو را بدهی، اولویت با Raw Tx است.</small>
    <button type="submit">Recover</button>
  </form>

  <?php if ($out): ?>
    <div class="result">
      <h3>نتیجه</h3>
      <p><b>نوع تراکنش:</b> <?=htmlspecialchars((string)$out['type'])?></p>
      <p><b>Public key (uncompressed):</b><br><?=htmlspecialchars($out['pub'])?></p>
      <p><b>Derived address:</b> <?=htmlspecialchars($out['address'])?></p>
      <?php if (array_key_exists('chainId', $out) && $out['chainId'] !== null): ?>
        <?php $chainVal = is_int($out['chainId']) ? (string)$out['chainId'] : (string)$out['chainId']; ?>
        <p><b>Chain ID:</b> <?=htmlspecialchars($chainVal === '' ? '—' : $chainVal)?></p>
      <?php endif; ?>
      <p><b>v:</b> <?=htmlspecialchars((string)($out['v'] ?? ''))?> |
         <b>r:</b> <?=htmlspecialchars($out['r'] ?? '')?> |
         <b>s:</b> <?=htmlspecialchars($out['s'] ?? '')?></p>
      <?php if (isset($out['recId'])): ?>
        <p><b>recId (used):</b> <?=htmlspecialchars((string)$out['recId'])?></p>
      <?php endif; ?>
      <?php if (!empty($out['alt_candidates']) && is_array($out['alt_candidates'])):
        $alts = array_filter($out['alt_candidates'], function($cand) use ($out) {
          return isset($cand['address']) && strtolower($cand['address']) !== strtolower($out['address']);
        });
        if ($alts): ?>
          <div>
            <small>آدرس‌های ممکن دیگر از امضا:</small>
            <ul>
              <?php foreach ($alts as $cand):
                $addrAlt = htmlspecialchars($cand['address']);
                $recAlt = htmlspecialchars((string)($cand['recId'] ?? '?'));
              ?>
                <li><?=$addrAlt?> (recId=<?=$recAlt?>)</li>
              <?php endforeach; ?>
            </ul>
          </div>
        <?php endif; ?>
      <?php endif; ?>
      <?php if (!empty($out['tx_from'])): ?>
        <p><b>tx.from:</b> <?=htmlspecialchars($out['tx_from'])?></p>
        <?php if (strtolower($out['tx_from']) === strtolower($out['address'])): ?>
          <p style="color:green"><b>✅ آدرس با from هم‌خوان است.</b></p>
        <?php else: ?>
          <p style="color:#b45309"><b>⚠️ mismatch با from.</b> (ممکن است v/recId یا ورودی نود متفاوت باشد)</p>
        <?php endif; ?>
      <?php endif; ?>
    </div>
  <?php endif; ?>
</div>
</body></html>
