/* global React, PageHeader, useLang, useT, fmtNumber, decodeTonswapLiquidity, TonswapProtocol */
(() => {
  const { useEffect, useRef, useState } = React;
  const TONSWAP_SITE = 'https://test.tonswap.org/';
  const MANIFEST = TONSWAP_SITE + 'release-manifest.json';
  // The published SCCP screen disables transfers in both directions for this release and web build.
  const SCCP_DISABLED_RELEASE = {
    id: 'testnet-ovh-20261104-639a09ce4da1',
    hash: '2e36913c48cb23358b281d2fad70ff816126147be2c9d874e5789d77ad5db5fb',
    webEntry: 'index-D1RCMYtW.js',
  };
  const INDEXER = 'https://ti.soramitsu.io/api/indexer/v1';
  const TON_ACCOUNTS = 'https://testnet.tonapi.io/v2/accounts';
  const TON_BLOCKCHAIN = 'https://testnet.tonapi.io/v2/blockchain';
  const TON_TRACES = 'https://testnet.tonapi.io/v2/traces/';
  const EXPLORER = 'https://testnet.tonscan.org';
  // Public address used in the official Tonswap testnet walkthrough.
  const EXAMPLE_WALLET = '0:3e94dfc963398c6bbbcdb0e640bbc9024dce1ddbb793605e87cdade07184479e';
  // Public wallet from the November testnet swap trace.
  const BALANCE_EXAMPLE = '0:b4872d375a401a48ecccfb1a5d989d92471a5d8b3b46368a88dfdb5979135f94';
  const CREATOR_EXAMPLE = '0:4169fa9e547a8e199e70c2bd6de5eca79a0b495365f50ab9e1fb6da7ad7da9c4';
  const WATCH_STORAGE = 'sm.tonswap.testnet.watchlist';
  const RAW_ADDRESS = /^(?:-1|0):[a-f0-9]{64}$/i;
  const FRIENDLY_ADDRESS = /^[a-z0-9+/_-]{48}$/i;
  const SWAP_LIMIT = 50;
  const CREATOR_PAGE_SIZE = 25;
  const WALLET_HISTORY_CACHE_MS = 30_000;
  const WALLET_HISTORY_CACHE_SIZE = 32;
  const WALLET_CREDIT_CACHE_MS = 5 * 60_000;
  // SVGs from the official testnet token selector (TokenIcon-DmDoxS8q.js).
  const TOKEN_ICONS = {
    GRAM: "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='80'%20height='80'%20fill='none'%20viewBox='0%200%2080%2080'%3e%3crect%20width='80'%20height='80'%20fill='%2330A1F5'%20rx='40'%20/%3e%3crect%20width='79.2'%20height='79.2'%20x='.4'%20y='.4'%20stroke='%23000'%20stroke-opacity='.06'%20stroke-width='.8'%20rx='39.6'%20/%3e%3cpath%20fill='%23fff'%20d='M48.328%2021.4H31.673c-2.218%200-3.327%200-4.33.31a7%207%200%200%200-2.423%201.323c-.804.676-1.404%201.61-2.603%203.475l-5.294%208.236c-.793%201.233-1.19%201.849-1.297%202.498a3.2%203.2%200%200%200%20.183%201.698c.243.61.76%201.13%201.797%202.165L37.38%2060.778c.918.918%201.377%201.377%201.906%201.549.465.151.966.151%201.432%200%20.529-.172.988-.631%201.905-1.549l19.673-19.673c1.037-1.036%201.555-1.554%201.798-2.165a3.2%203.2%200%200%200%20.182-1.698c-.107-.649-.504-1.265-1.296-2.498l-5.294-8.236c-1.2-1.866-1.8-2.799-2.603-3.475a7%207%200%200%200-2.423-1.323c-1.003-.31-2.112-.31-4.33-.31'%20/%3e%3cpath%20fill='%2330A1F5'%20d='M45.173%2027.897c.27-.731%201.304-.731%201.575%200l1.87%205.054c.112.303.352.542.655.655l5.054%201.87c.731.27.731%201.305%200%201.575l-5.054%201.87a1.1%201.1%200%200%200-.655.655l-1.87%205.054c-.27.731-1.305.731-1.575%200l-1.87-5.054a1.1%201.1%200%200%200-.656-.655l-5.053-1.87c-.732-.27-.732-1.305%200-1.575l5.053-1.87c.304-.113.543-.352.656-.655z'%20/%3e%3c/svg%3e",
    T3: "data:image/svg+xml,%3csvg%20width='48'%20height='48'%20viewBox='0%200%2048%2048'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3ccircle%20cx='24'%20cy='24'%20r='24'%20fill='%232458EB'/%3e%3cpath%20d='M10.5%2016.5H25M17.75%2016.5V33'%20stroke='white'%20stroke-width='4.2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cpath%20d='M29.5%2016.5H38.5L33.4%2022.6C36.6%2022.6%2039%2024.7%2039%2027.8C39%2031%2036.4%2033.5%2033%2033.5C31%2033.5%2029.4%2032.8%2028.2%2031.6'%20stroke='white'%20stroke-width='4.2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3c/svg%3e",
    TS: "data:image/svg+xml,%3csvg%20width='48'%20height='48'%20viewBox='0%200%2048%2048'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M1.09669e-06%2024C5.17322e-07%2037.2545%2010.7449%2048%2023.9994%2048C37.2538%2048%2048%2037.2558%2048%2024C48%2010.7442%2037.2551%20-4.69673e-07%2024.0006%20-1.04904e-06C10.7462%20-1.62842e-06%201.67607e-06%2010.7455%201.09669e-06%2024Z'%20fill='%230098EA'/%3e%3cpath%20d='M32.0124%2013.9779C32.0124%2013.9779%2015.1708%2010.9095%2011.354%2012.0268C7.53725%2013.1447%205.30214%2017.396%206.38027%2021.4842C7.4584%2025.5725%2011.4561%2027.9994%2015.2723%2026.8808C19.089%2025.7629%2032.0124%2013.9779%2032.0124%2013.9779ZM15.823%2033.3699C15.823%2033.3699%2032.5657%2037.0331%2036.416%2036.051C40.2651%2035.0702%2042.6321%2030.9008%2041.6806%2026.7775C40.7292%2022.6548%2036.8084%2020.0888%2032.9586%2021.0709C29.1089%2022.0517%2015.8236%2033.3693%2015.8236%2033.3693L15.823%2033.3699Z'%20fill='white'/%3e%3c/svg%3e",
    USDT: "data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'?%3e%3csvg%20id='Layer_2'%20data-name='Layer%202'%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20800%20800'%3e%3cdefs%3e%3cstyle%3e%20.cls-1%20{%20fill:%20%23009393;%20}%20.cls-1,%20.cls-2%20{%20stroke-width:%200px;%20}%20.cls-2%20{%20fill:%20%23fff;%20fill-rule:%20evenodd;%20}%20%3c/style%3e%3c/defs%3e%3cg%20id='Layer_1-2'%20data-name='Layer%201'%3e%3cg%3e%3ccircle%20class='cls-1'%20cx='400'%20cy='400'%20r='400'/%3e%3cpath%20class='cls-2'%20d='M400.49,428.59c68.79,0,126.28-11.63,140.33-27.17-11.93-13.18-55.08-23.56-109.88-26.4v32.83c-9.81.51-20.01.76-30.46.76s-20.65-.25-30.48-.76v-32.83c-54.78,2.84-97.95,13.22-109.88,26.4,14.07,15.54,71.57,27.17,140.36,27.17ZM522.71,274.06v45.21h-91.77v31.35c64.46,3.35,112.83,17.13,113.19,33.62v34.38c-.36,16.49-48.73,30.24-113.19,33.6v76.94h-60.93v-76.94c-64.46-3.35-112.81-17.11-113.17-33.6v-34.38c.36-16.49,48.71-30.27,113.17-33.62v-31.35h-91.77v-45.21h244.48ZM242.15,202.11h322.16c7.7,0,14.79,4.05,18.63,10.63l93.85,161.16c4.86,8.36,3.42,18.91-3.52,25.68l-258.34,252.18c-8.38,8.17-21.84,8.17-30.2,0L126.71,399.92c-7.09-6.94-8.43-17.79-3.2-26.19l100.33-161.49c3.91-6.28,10.85-10.12,18.32-10.12Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e",
    USDC: "data:image/svg+xml,%3csvg%20width='96'%20height='96'%20viewBox='0%200%2096%2096'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M48%2095C73.9574%2095%2095%2073.9574%2095%2048C95%2022.0426%2073.9574%201%2048%201C22.0426%201%201%2022.0426%201%2048C1%2073.9574%2022.0426%2095%2048%2095Z'%20fill='%230B53BF'/%3e%3cpath%20d='M56.4609%2013.7778V19.8291C68.5341%2023.4716%2077.3759%2034.6928%2077.3759%2047.9997C77.3759%2061.3066%2068.5341%2072.5278%2056.4609%2076.1703V82.2216C71.8534%2078.4616%2083.2509%2064.5672%2083.2509%2047.9997C83.2509%2031.4322%2071.8534%2017.5378%2056.4609%2013.7778Z'%20fill='white'/%3e%3cpath%20d='M18.625%2047.9997C18.625%2034.6928%2027.4669%2023.4716%2039.54%2019.8291V13.7778C24.1475%2017.5378%2012.75%2031.4322%2012.75%2047.9997C12.75%2064.5672%2024.1475%2078.4616%2039.54%2082.2216V76.1703C27.4669%2072.5572%2018.625%2061.3066%2018.625%2047.9997Z'%20fill='white'/%3e%3cpath%20d='M60.6319%2054.5506C60.6319%2042.5362%2041.8025%2047.4713%2041.8025%2040.8325C41.8025%2038.4531%2043.7119%2036.9256%2047.3544%2036.9256C51.7019%2036.9256%2053.2%2039.0406%2053.67%2041.89H59.6625C59.1279%2036.5426%2056.0588%2033.1662%2050.9382%2032.1604V27.4375H45.0632V31.9918C39.4534%2032.7062%2035.9275%2035.973%2035.9275%2040.8325C35.9275%2052.9056%2054.7863%2048.3819%2054.7863%2054.9031C54.7863%2057.3706%2052.4069%2059.0156%2048.3825%2059.0156C43.1244%2059.0156%2041.3913%2056.695%2040.745%2053.4931H34.8994C35.2781%2059.3502%2038.8897%2063.0159%2045.0632%2063.9307V68.5625H50.9382V63.9923C56.9633%2063.2139%2060.6319%2059.7089%2060.6319%2054.5506Z'%20fill='white'/%3e%3c/svg%3e",
  };
  const PROTOCOL_CONTRACTS = {
    perps: ['PerpsEngine', '2d3904daaf43529a87f9594d753641ffff880f1c75ce11d8505576d326883069'],
    launchpad: ['SaleFactory', 'ca26175c1c8b216b8b8dc1e7c12dce20a5d72978c3a867716ebb9db0dc169844'],
    t3: ['T3Hub', 'b2fb1bebba68bfd5444424f914f22e2a4d6ac0cf0a69c1803106ea25ef0b67cb'],
    options: ['OptionVault', '2db5e0199689f4561850c048a523fc526b28a615869de3ec64c8977cdbf53a77'],
    orders: ['SpotOrderFactory', '42ba761d87b32874205d59bee2ab4603cbb899e19b2a26efb2eb4601d90dec11'],
  };
  const SPOT_ORDER_HASH = '48973db561a87f955d55a1e1b20ddc2a52148e9aad7f3bf3e2478761b7b6e442';
  const OPTION_FACTORY_HASH = 'a62a03a709068f957ec7657056e52dc5cb0cf4bba8cc9b315d152bc07377b162';
  const FAUCET_HASH = 'df7838b72e21688cff6cbbcb3376f0cce0a71878adb6d990cc6796b7a884b6c5';

  async function readResponse(url, signal, body) {
    const options = body === undefined ? {} : {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    };
    const response = await fetch(url, {
      ...options,
      credentials: 'omit',
      signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]),
    });
    if (!response.ok) throw Object.assign(new Error('Tonswap request failed'), { status: response.status });
    return response;
  }

  async function readJson(url, signal, body) {
    return (await readResponse(url, signal, body)).json();
  }

  function releaseTokens(manifest) {
    if (manifest.schema !== 'tonswap-release-v1' || manifest.network !== 'ton:testnet' ||
        manifest.globalId !== -3 || !Array.isArray(manifest.tokens)) {
      throw new Error('Unexpected Tonswap release manifest');
    }
    return manifest.tokens;
  }

  const validDecimals = value => Number.isInteger(value) && value >= 0 && value <= 30;

  function marketCatalog(manifest) {
    const tokens = new Map(releaseTokens(manifest).map(token => [token.symbol, token.decimals]));
    if (!Array.isArray(manifest.spotMarkets)) throw new Error('Unexpected Tonswap markets');
    if (!validDecimals(tokens.get('T3'))) throw new Error('T3 precision is unavailable');
    const treasury = manifest.contracts?.Treasury?.address;
    return manifest.spotMarkets.map(({ symbol, pool, xDecimals, feePips }) => {
      if (!RAW_ADDRESS.test(pool) || !/^[A-Z0-9]{1,12}$/.test(symbol) ||
          !validDecimals(xDecimals) || tokens.get(symbol) !== xDecimals ||
          !Number.isInteger(feePips) || feePips < 0 || feePips > 1_000_000) {
        throw new Error('Unexpected Tonswap market configuration');
      }
      return { symbol, pool, xDecimals, t3Decimals: tokens.get('T3'), feePips, treasury };
    });
  }

  function poolReserves({ exit_code, stack }) {
    // Current DLMM get_pool_info returns 14 items, with reserveT and reserveX at 8 and 9.
    const value = item => item?.[0] === 'num' && typeof item[1] === 'string' && /^\d+$/.test(item[1]);
    if (exit_code !== 0 || stack?.length !== 14 || stack[0]?.[0] !== 'cell' ||
        !value(stack[8]) || !value(stack[9])) {
      throw new Error('Unexpected DLMM reserve response');
    }
    return { t3: BigInt(stack[8][1]), asset: BigInt(stack[9][1]) };
  }

  function formatTokenAmount(raw, decimals, locale) {
    const digits = String(raw).padStart(decimals + 1, '0');
    const split = digits.length - decimals;
    const whole = BigInt(digits.slice(0, split));
    const fraction = digits.slice(split).replace(/0+$/, '');
    const format = new Intl.NumberFormat(locale);
    const decimalMark = format.formatToParts(1.1).find(part => part.type === 'decimal').value;
    const digitFormat = new Intl.NumberFormat(locale, { useGrouping: false });
    return format.format(whole) + (fraction ? decimalMark + fraction.replace(/\d/g, digit => digitFormat.format(digit)) : '');
  }

  function transactionHash(hash) {
    if (typeof hash !== 'string') return null;
    if (/^[a-f0-9]{64}$/i.test(hash)) return hash.toLowerCase();
    if (!/^[a-z0-9+/_-]{43}=?$/i.test(hash)) return null;
    const bytes = atob(hash.replace(/-/g, '+').replace(/_/g, '/'));
    return Array.from(bytes, byte => byte.charCodeAt(0).toString(16).padStart(2, '0')).join('');
  }

  function TonTokenIcon({ symbol = '?', showLogo = true }) {
    const icon = showLogo && Object.hasOwn(TOKEN_ICONS, symbol) ? TOKEN_ICONS[symbol] : null;
    return <span className="tonswap-token" data-asset={icon ? symbol : undefined} aria-hidden="true">
      {icon ? <img src={icon} alt="" width="28" height="28"/> : Array.from(symbol).slice(0, 2).join('')}
    </span>;
  }

  function PoolCard({ market, reserves, onActivity }) {
    const { t, locale } = useLang();
    const funded = reserves && (reserves.asset > 0n || reserves.t3 > 0n);
    const state = reserves ? (funded ? 'liquid' : 'noLiquidity') : 'unavailable';
    const percent = fmtNumber(market.feePips / 1_000_000, locale, { style: 'percent', maximumFractionDigits: 4 });
    return (
      <article className="card tonswap-pool">
        <header className="tonswap-pool-header">
          <div className="tonswap-pair">
            <div className="tonswap-pair-icons" aria-hidden="true">
              <TonTokenIcon symbol={market.symbol}/>
              <TonTokenIcon symbol="T3"/>
            </div>
            <h3>{market.symbol} / T3</h3>
          </div>
          <span className={funded ? 'tag ok' : 'tag'}>{t('tonswap.' + state)}</span>
        </header>
        <div className="tonswap-reserves">
          <p>{t('tonswap.reserves')}</p>
          <dl>
            <div><dt>{market.symbol}</dt><dd>{reserves ? formatTokenAmount(reserves.asset, market.xDecimals, locale) : '—'}</dd></div>
            <div><dt>T3</dt><dd>{reserves ? formatTokenAmount(reserves.t3, market.t3Decimals, locale) : '—'}</dd></div>
          </dl>
        </div>
        <footer className="tonswap-pool-footer">
          <span>{t('tonswap.fee')} <strong>{percent}</strong></span>
          <div className="tonswap-pool-actions">
            <a href={EXPLORER + '/address/' + encodeURIComponent(market.pool)} target="_blank" rel="noopener noreferrer">
              {t('tonswap.viewContract')} <span aria-hidden="true">↗</span>
            </a>
            <button type="button" className="btn tonswap-activity-button" aria-haspopup="dialog" onClick={onActivity}>
              {t('tonswap.activity.title')}
            </button>
          </div>
        </footer>
      </article>
    );
  }

  function TonAddressLink({ address, label }) {
    return (
      <a className="tonswap-activity-address" href={EXPLORER + '/address/' + encodeURIComponent(address)}
        target="_blank" rel="noopener noreferrer" title={address}>
        {label && <strong>{label}</strong>}
        <bdi>{address.slice(0, 8)}…{address.slice(-4)}</bdi>
      </a>
    );
  }

  function ActivityRow({ row, market, dateFormat }) {
    const { t, locale } = useLang();
    const action = { 1: 'deposit', 2: 'withdrawal', 3: 'eviction' }[row.kind];
    const date = new Date(row.utime * 1000);
    return (
      <li className="tonswap-activity-row">
        <div className="tonswap-activity-cell">
          <strong className={'tonswap-activity-kind ' + action}>{t('tonswap.activity.' + action)}</strong>
          <time dateTime={date.toISOString()}>{dateFormat.format(date)}</time>
        </div>
        <div className="tonswap-activity-cell tonswap-activity-amount">
          {row.asset !== 0n && <bdi>{formatTokenAmount(row.asset, market.xDecimals, locale)} {market.symbol}</bdi>}
          {row.t3 !== 0n && <bdi>{formatTokenAmount(row.t3, market.t3Decimals, locale)} T3</bdi>}
          {row.asset === 0n && row.t3 === 0n && <span>—</span>}
        </div>
        <div className="tonswap-activity-cell" role="group" aria-label={t('tonswap.activity.owner')}>
          <small className="tonswap-activity-owner-label" aria-hidden="true">{t('tonswap.activity.owner')}</small>
          <TonAddressLink address={row.owner} label={row.owner === market.treasury ? t('tonswap.activity.treasury') : undefined}/>
          {row.payer && row.payer !== row.owner && (
            <small>{t('tonswap.activity.payer')} <TonAddressLink address={row.payer}/></small>
          )}
        </div>
        <a className="tonswap-activity-tx" href={EXPLORER + '/tx/' + row.hash} target="_blank" rel="noopener noreferrer">
          {t('tonswap.viewTx')} <span aria-hidden="true">↗</span>
        </a>
      </li>
    );
  }

  function PoolActivity({ market, onClose }) {
    const { t, locale } = useLang();
    const dialog = useRef(null);
    const [request, setRequest] = useState({ cursor: null });
    const [data, setData] = useState({ rows: [], loading: true });
    const dateFormat = new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' });
    useEffect(() => {
      const element = dialog.current;
      element.showModal();
      return () => element.close();
    }, []);

    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
      const anchor = request.cursor;
      const query = new URLSearchParams(anchor ? { cursor_lt: anchor.lt, cursor_hash: anchor.hash } : { page: '1' });
      const url = INDEXER + '/accounts/' + encodeURIComponent(market.pool) + '/txs?' + query;
      async function load() {
        try {
          const page = await readJson(url, signal);
          if (page.network !== 'testnet' || !Array.isArray(page.txs) ||
              !Number.isInteger(page.page_size) || page.page_size < 1) {
            throw new Error('Unexpected Tonswap account history');
          }
          const decoded = decodeTonswapLiquidity(page.txs);
          const rows = decoded.rows.map(row => ({ ...row, hash: transactionHash(row.hash) }))
            .filter(row => row.hash && Number.isSafeInteger(row.utime) && row.utime > 0 && row.utime < 253402300800);
          const last = page.txs.at(-1);
          const more = page.txs.length >= page.page_size &&
            (anchor || (page.total_pages ?? page.total_pages_min) > 1 || page.history_complete !== true);
          const next = more && typeof last?.lt === 'string' && /^\d+$/.test(last.lt) && transactionHash(last.hash) &&
            (!anchor || last.lt !== anchor.lt) ? { lt: last.lt, hash: last.hash } : null;
          const incomplete = decoded.incomplete || rows.length !== decoded.rows.length ||
            page.history_complete !== true || (more && !next);
          if (signal.aborted) return;
          setData(previous => {
            // Soramitsu's cursor includes the boundary transaction.
            const all = anchor ? previous.rows.concat(rows) : rows;
            const unique = new Map(all.map(row => [row.hash + ':' + row.key, row]));
            return { rows: [...unique.values()], next, incomplete: incomplete || Boolean(anchor && previous.incomplete) };
          });
        } catch {
          if (!signal.aborted) setData(previous => ({ ...previous, loading: false, error: true }));
        }
      }
      load();
      return () => controller.abort();
    }, [market.pool, request]);

    function loadPage(cursor) {
      setData(previous => ({ ...previous, loading: true, error: false }));
      setRequest({ cursor });
    }

    function close() {
      dialog.current.close();
      onClose();
    }

    return (
      <dialog ref={dialog} className="sm-modal tonswap-activity-dialog" aria-labelledby="tonswap-activity-title"
        onCancel={event => { event.preventDefault(); close(); }}
        onClick={event => { if (event.target === event.currentTarget) close(); }}>
        <header className="sm-modal-head">
          <div>
            <h3 id="tonswap-activity-title">{t('tonswap.activity.title')} · {market.symbol} / T3</h3>
            <span className="tag info">{t('tonswap.testnet')}</span>
          </div>
          <button className="sm-modal-x" type="button" onClick={close} aria-label={t('common.close')}>×</button>
        </header>
        <div className="sm-modal-body" aria-busy={Boolean(data.loading)}>
          {data.loading && <p className="tonswap-note" role="status">{t('pool.activity.loading')}</p>}
          {data.error && <div className="tonswap-activity-error">
            <p className="tonswap-note" role="alert">{t('tonswap.activity.error')}</p>
            <button className="btn" type="button" onClick={() => loadPage(request.cursor)}>{t('common.retry')}</button>
          </div>}
          {!data.loading && data.incomplete && <p className="tonswap-note" role="status">{t('tonswap.activity.incomplete')}</p>}
          {!data.loading && !data.error && !data.rows.length && (
            <p className="tonswap-note" role="status">{t(data.incomplete || data.next ? 'tonswap.activity.emptyPage' : 'tonswap.activity.empty')}</p>
          )}
          {data.rows.length > 0 && (
            <div>
              <div className="tonswap-activity-columns" aria-hidden="true">
                <span>{t('pool.col.type')}</span><span>{t('pool.col.amount')}</span>
                <span>{t('tonswap.activity.owner')}</span><span>{t('tonswap.activity.transaction')}</span>
              </div>
              <ol className="tonswap-activity-list" role="list">
                {data.rows.map(row => <ActivityRow key={row.hash + ':' + row.key} row={row} market={market} dateFormat={dateFormat}/>)}
              </ol>
            </div>
          )}
          {data.rows.some(row => row.kind !== 1) && <p className="tonswap-note">{t('tonswap.activity.withdrawalNote')}</p>}
          {data.next && !data.error && (
            <button type="button" className="btn tonswap-activity-more" disabled={data.loading}
              onClick={() => loadPage(data.next)}>{t('tonswap.activity.more')}</button>
          )}
        </div>
      </dialog>
    );
  }

  function TonswapPools() {
    const { t, locale } = useLang();
    const [revision, setRevision] = useState(0);
    const [data, setData] = useState({ loading: true, rows: [] });
    const [activeMarket, setActiveMarket] = useState(null);

    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
      async function load() {
        try {
          const markets = marketCatalog(await readJson(MANIFEST, signal));
          const responses = await Promise.allSettled(markets.map(async market => poolReserves(
            await readJson(INDEXER + '/runGetMethod', signal, { address: market.pool, method: 'get_pool_info', stack: [] }),
          )));
          if (signal.aborted) return;
          const rows = markets.map((market, index) => {
            const response = responses[index];
            return { market, reserves: response.status === 'fulfilled' ? response.value : null };
          });
          setData({ rows, checkedAt: Date.now() });
        } catch {
          if (!signal.aborted) setData({ error: true, rows: [] });
        }
      }
      load();
      return () => controller.abort();
    }, [revision]);

    function refresh() {
      setData(current => ({ ...current, loading: true, error: false }));
      setRevision(current => current + 1);
    }

    return (
      <section aria-labelledby="tonswap-pools-title" aria-busy={Boolean(data.loading)}>
        <div className="tonswap-toolbar">
          <div>
            <h2 id="tonswap-pools-title">{t('tonswap.poolHeading')}</h2>
            <p>{t('tonswap.poolDescription')}</p>
          </div>
          <div className="tonswap-toolbar-actions">
            {data.checkedAt && <small>{t('tonswap.updated').replace('{time}', new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(data.checkedAt))}</small>}
            <button type="button" className="btn" onClick={refresh} disabled={data.loading}>{t('tonswap.refresh')}</button>
          </div>
        </div>
        {data.loading && <p className="tonswap-note" role="status">{t('tonswap.loadingPools')}</p>}
        {data.error && <p className="tonswap-note" role="alert">{t('tonswap.poolError')}</p>}
        {!data.loading && !data.error && !data.rows.length && <p className="tonswap-note">{t('tonswap.noPools')}</p>}
        <div className="tonswap-pool-grid">
          {data.rows.map(({ market, reserves }) => (
            <PoolCard key={market.pool} market={market} reserves={reserves} onActivity={() => setActiveMarket(market)}/>
          ))}
        </div>
        {activeMarket && <PoolActivity market={activeMarket} onClose={() => setActiveMarket(null)}/>}
        {!data.loading && data.rows.some(row => !row.reserves) && <p className="tonswap-note" role="status">{t('tonswap.poolPartial')}</p>}
      </section>
    );
  }

  function WalletHistory({ history }) {
    const { t, locale } = useLang();
    const statusTags = { success: 'ok', failed: 'err', pending: 'warn' };
    const dateFormat = new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' });
    return (
      <>
        {!history.history_complete && <p className="tonswap-note" role="status">{t('tonswap.coverage')}</p>}
        {history.swaps.length === 0 ? <p className="tonswap-note" role="status">{t('tonswap.noSwaps')}</p> : (
          <div className="card tonswap-history">
            <div className="tonswap-history-count" role="status">
              {t('tonswap.swapCount').replace('{shown}', fmtNumber(history.swaps.length, locale)).replace('{total}', fmtNumber(history.total_swaps, locale))}
            </div>
            <ol>
              {history.swaps.map(swap => {
                const hash = transactionHash(swap.hash);
                const timestamp = Number(swap.utime) * 1000;
                const date = new Date(timestamp);
                const validDate = timestamp > 0 && Number.isFinite(date.getTime());
                const status = Object.hasOwn(statusTags, swap.status) ? swap.status : null;
                return (
                  <li className="tonswap-swap" key={swap.txId}>
                    <strong className="tonswap-swap-pair">{swap.payToken || '—'} <span aria-hidden="true">→</span> {swap.receiveToken || '—'}</strong>
                    <span className={status ? 'tag ' + statusTags[status] : 'tag'}>{t(status ? 'tonswap.' + status : 'tonswap.unavailable')}</span>
                    <time dateTime={validDate ? date.toISOString() : undefined}>{validDate ? dateFormat.format(date) : '—'}</time>
                    {hash && <a href={EXPLORER + '/tx/' + hash} target="_blank" rel="noopener noreferrer">
                      {t('tonswap.viewTx')} <span aria-hidden="true">↗</span>
                    </a>}
                  </li>
                );
              })}
            </ol>
          </div>
        )}
      </>
    );
  }

  function TonswapWallet() {
    const t = useT();
    const [address, setAddress] = useState('');
    const [result, setResult] = useState({});
    const request = useRef(null);
    useEffect(() => () => request.current?.abort(), []);

    async function lookup(value) {
      request.current?.abort();
      const wallet = value.trim();
      if (!RAW_ADDRESS.test(wallet) && !FRIENDLY_ADDRESS.test(wallet)) {
        setResult({ error: 'tonswap.invalidAddress' });
        return;
      }
      const controller = new AbortController();
      request.current = controller;
      setResult({ loading: true });
      try {
        const data = await readJson(INDEXER + '/accounts/' + encodeURIComponent(wallet) + '/swaps?limit=' + SWAP_LIMIT, controller.signal);
        if (data.network !== 'testnet' || !Array.isArray(data.swaps) || !Number.isInteger(data.total_swaps)) {
          throw new Error('Unexpected wallet swap history');
        }
        if (!controller.signal.aborted) setResult({ history: data });
      } catch (error) {
        if (!controller.signal.aborted) setResult({ error: error.status === 400 ? 'tonswap.invalidAddress' : 'tonswap.walletError' });
      }
    }

    return (
      <section aria-labelledby="tonswap-wallet-title">
        <div className="tonswap-toolbar">
          <div>
            <h2 id="tonswap-wallet-title">{t('tonswap.walletSwaps')}</h2>
            <p>{t('tonswap.walletHint')}</p>
          </div>
        </div>
        <form className="card tonswap-wallet-form" onSubmit={event => { event.preventDefault(); lookup(address); }}>
          <label htmlFor="tonswap-wallet">{t('tonswap.address')}</label>
          <div className="tonswap-wallet-input-row">
            <input id="tonswap-wallet" dir="ltr" value={address} onChange={event => setAddress(event.target.value)}
              maxLength={96} placeholder="0:… / kQ…" spellCheck={false} autoComplete="off" autoCapitalize="off"
              aria-invalid={result.error === 'tonswap.invalidAddress' || undefined}
              aria-describedby={result.error ? 'tonswap-wallet-error' : undefined}/>
            <button className="btn primary" type="submit">{t('tonswap.search')}</button>
          </div>
          <button className="tonswap-example" type="button" onClick={() => { setAddress(EXAMPLE_WALLET); lookup(EXAMPLE_WALLET); }}>
            {t('tonswap.example')}
          </button>
          {result.error && <p className="tonswap-wallet-error" id="tonswap-wallet-error" role="alert">{t(result.error)}</p>}
        </form>
        <div className="tonswap-wallet-results" aria-busy={Boolean(result.loading)}>
          <p className="tonswap-note">{t('tonswap.walletScope')}</p>
          {result.loading && <p className="tonswap-note" role="status">{t('tonswap.loadingSwaps')}</p>}
          {result.history && <WalletHistory history={result.history}/>}
        </div>
      </section>
    );
  }

  function canonicalTonAddress(value) {
    const address = value.trim();
    if (RAW_ADDRESS.test(address)) return address.toLowerCase();
    if (!FRIENDLY_ADDRESS.test(address)) return null;
    const bytes = Uint8Array.from(atob(address.replace(/-/g, '+').replace(/_/g, '/')), char => char.charCodeAt(0));
    const tag = bytes[0] & 0x7f;
    if (bytes.length !== 36 || (tag !== 0x11 && tag !== 0x51) || (bytes[1] !== 0 && bytes[1] !== 255)) return null;
    let checksum = 0;
    for (const byte of bytes.subarray(0, 34)) {
      checksum ^= byte << 8;
      for (let bit = 0; bit < 8; bit++) checksum = ((checksum << 1) ^ ((checksum & 0x8000) ? 0x1021 : 0)) & 0xffff;
    }
    if (checksum !== (bytes[34] << 8 | bytes[35])) return null;
    const hash = Array.from(bytes.subarray(2, 34), byte => byte.toString(16).padStart(2, '0')).join('');
    return (bytes[1] === 0 ? '0:' : '-1:') + hash;
  }

  function watchedWallets() {
    try {
      const items = JSON.parse(localStorage.getItem(WATCH_STORAGE));
      if (Array.isArray(items)) return items.filter(item => item && RAW_ADDRESS.test(item.address) && typeof item.label === 'string');
    } catch {}
    return [];
  }

  async function readTonPortfolio(address, signal) {
    const url = TON_ACCOUNTS + '/' + encodeURIComponent(address);
    const [account, assets, manifest] = await Promise.allSettled([
      readJson(url, signal),
      readJson(url + '/jettons', signal),
      readJson(MANIFEST, signal).then(releaseTokens),
    ]);
    const native = account.status === 'fulfilled' && account.value?.address === address &&
      Number.isSafeInteger(account.value.balance) && account.value.balance >= 0
      ? BigInt(account.value.balance) : null;
    if (assets.status !== 'fulfilled' || !Array.isArray(assets.value?.balances) || manifest.status !== 'fulfilled') {
      return { native, holdings: null, incomplete: true };
    }
    const current = new Map(manifest.value.filter(token => RAW_ADDRESS.test(token?.root))
      .map(token => [token.root.toLowerCase(), token]));
    const holdings = new Map();
    let incomplete = native === null;
    for (const entry of assets.value.balances) {
      const address = entry?.jetton?.address;
      const raw = entry?.balance;
      if (typeof address !== 'string' || !RAW_ADDRESS.test(address) || typeof raw !== 'string' || !/^\d{1,78}$/.test(raw)) {
        incomplete = true;
        continue;
      }
      const amount = BigInt(raw);
      if (amount === 0n) continue;
      const root = address.toLowerCase();
      const official = current.get(root);
      const metadata = official || entry.jetton;
      const symbol = typeof metadata.symbol === 'string' ? metadata.symbol.trim().slice(0, 24) : '';
      const decimals = validDecimals(metadata.decimals) ? metadata.decimals : null;
      incomplete ||= decimals === null;
      holdings.set(root, { root, symbol: symbol || 'Jetton', amount, decimals, current: Boolean(official) });
    }
    const rows = [...holdings.values()].sort((left, right) => Number(right.current) - Number(left.current));
    return { native, holdings: rows, incomplete };
  }

  function TonWatchlist({ entries, active, onOpen }) {
    const t = useT();
    return (
      <aside className="card tonswap-watchlist" aria-labelledby="tonswap-watchlist-title">
        <h3 id="tonswap-watchlist-title">{t('tonswap.portfolio.watchlist')}</h3>
        <p>{t('tonswap.portfolio.local')}</p>
        {entries.length === 0 ? <p>{t('tonswap.portfolio.watchEmpty')}</p> : (
          <ul role="list">
            {entries.map(entry => (
              <li key={entry.address}>
                <button type="button" className="btn" aria-pressed={active === entry.address}
                  onClick={() => onOpen(entry.address)}>
                  {entry.label && <strong>{entry.label}</strong>}
                  <bdi>{entry.address.slice(0, 10)}…{entry.address.slice(-6)}</bdi>
                </button>
              </li>
            ))}
          </ul>
        )}
      </aside>
    );
  }

  function TonPortfolioBalances({ result, onRefresh }) {
    const { t, locale } = useLang();
    const assets = [{ symbol: 'GRAM', amount: result.native, decimals: 9 }, ...(result.holdings || [])];
    return (
      <>
        <div className="tonswap-wallet-toolbar">
          <p className="muted tiny">{t('tonswap.portfolio.assets')}</p>
          <button type="button" className="btn" aria-label={t('tonswap.portfolio.refresh')} onClick={onRefresh}>
            {t('tonswap.refresh')}
          </button>
        </div>
        <table className="lp-table tonswap-wallet-table tonswap-wallet-assets-table">
          <thead><tr>
            <th scope="col">{t('tonswap.portfolio.asset')}</th>
            <th scope="col">{t('s.balance')}</th>
          </tr></thead>
          <tbody>
            {assets.map(asset => (
              <tr key={asset.root ?? 'native'}>
                <td>
                  <div className="tonswap-wallet-asset">
                    <TonTokenIcon symbol={asset.symbol} showLogo={!asset.root || asset.current}/>
                    <div>
                      <div className="tonswap-wallet-asset-name">
                        <strong>{asset.symbol}</strong>
                        {asset.current && <span className="tag info">{t('tonswap.portfolio.current')}</span>}
                      </div>
                      {asset.root ? <TonAddressLink address={asset.root}/> : <small className="muted">{t('tonswap.portfolio.native')}</small>}
                    </div>
                  </div>
                </td>
                <td>
                  <bdi className="num">
                    {asset.amount === null || asset.decimals === null ? '—' : formatTokenAmount(asset.amount, asset.decimals, locale)}
                  </bdi>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        {result.holdings === null && <p className="muted tiny">{t('tonswap.portfolio.noTokenData')}</p>}
        {result.holdings?.length === 0 && <p className="muted tiny">{t('tonswap.portfolio.noTokens')}</p>}
      </>
    );
  }

  function TonWalletPanels({ result, cache, onRefresh }) {
    const t = useT();
    const [pane, setPane] = useState('assets');
    const [activityOpened, setActivityOpened] = useState(false);
    const panes = ['assets', 'activity'];

    function selectPane(next) {
      setPane(next);
      if (next === 'activity') setActivityOpened(true);
    }

    function handleTabKey(event) {
      const index = panes.indexOf(pane);
      const move = { ArrowLeft: 1 - index, ArrowRight: 1 - index, Home: 0, End: panes.length - 1 };
      const next = move[event.key];
      if (!Number.isInteger(next)) return;
      event.preventDefault();
      event.currentTarget.querySelectorAll('[role="tab"]')[next].focus();
      selectPane(panes[next]);
    }

    return (
      <>
        <div className="sm-modal-tabs tonswap-wallet-tabs" role="tablist"
          aria-label={t('tonswap.portfolio.title')} onKeyDown={handleTabKey}>
          {panes.map(id => (
            <button key={id} type="button" role="tab" id={'ton-wallet-tab-' + id}
              aria-controls={'ton-wallet-pane-' + id} aria-selected={pane === id} tabIndex={pane === id ? 0 : -1}
              className={'sm-modal-tab' + (pane === id ? ' active' : '')} onClick={() => selectPane(id)}>
              {t('tonswap.portfolio.tab.' + id)}
            </button>
          ))}
        </div>
        <div className="tonswap-wallet-pane tonswap-wallet-assets" role="tabpanel" id="ton-wallet-pane-assets"
          aria-labelledby="ton-wallet-tab-assets" hidden={pane !== 'assets'}>
          {result.loading && <p className="tonswap-note" role="status">{t('tonswap.portfolio.loading')}</p>}
          {result.balances && (
            <>
              {result.balances.incomplete && (
                <div className="tonswap-portfolio-retry">
                  <p className="tonswap-note" role="status">{t('tonswap.portfolio.incomplete')}</p>
                  <button type="button" className="btn" onClick={onRefresh}>{t('common.retry')}</button>
                </div>
              )}
              <TonPortfolioBalances result={result.balances} onRefresh={onRefresh}/>
            </>
          )}
        </div>
        <div className="tonswap-wallet-pane" role="tabpanel" id="ton-wallet-pane-activity"
          aria-labelledby="ton-wallet-tab-activity" hidden={pane !== 'activity'}>
          {activityOpened && <TonWalletHistory wallet={result.address} cache={cache}/>}
        </div>
      </>
    );
  }

  function TonswapPortfolio() {
    const t = useT();
    const [address, setAddress] = useState('');
    const [alias, setAlias] = useState('');
    const [watchlist, setWatchlist] = useState(watchedWallets);
    const [saveError, setSaveError] = useState(false);
    const [result, setResult] = useState({});
    const request = useRef(null);
    const historyCache = useRef(new Map());
    const watched = watchlist.find(entry => entry.address === result.address);

    useEffect(() => {
      const sync = event => {
        if (event.key === WATCH_STORAGE || event.key === null) setWatchlist(watchedWallets());
      };
      window.addEventListener('storage', sync);
      return () => {
        window.removeEventListener('storage', sync);
        request.current?.abort();
      };
    }, []);

    async function lookup(value) {
      request.current?.abort();
      setAlias('');
      setSaveError(false);
      const wallet = canonicalTonAddress(value);
      if (!wallet) {
        setResult({ invalid: true });
        return;
      }
      const controller = new AbortController();
      request.current = controller;
      setResult({ address: wallet, loading: true });
      try {
        const balances = await readTonPortfolio(wallet, controller.signal);
        if (!controller.signal.aborted) setResult({ address: wallet, balances });
      } catch {
        if (!controller.signal.aborted) setResult({ address: wallet, balances: { native: null, holdings: null, incomplete: true } });
      }
    }

    function openWallet(wallet) {
      setAddress(wallet);
      lookup(wallet);
    }

    function toggleFollow() {
      const items = watchedWallets();
      const next = items.some(item => item.address === result.address)
        ? items.filter(item => item.address !== result.address)
        : [...items, { address: result.address, label: alias.trim() }];
      try {
        localStorage.setItem(WATCH_STORAGE, JSON.stringify(next));
        setWatchlist(next);
        setSaveError(false);
      } catch {
        setSaveError(true);
      }
    }

    return (
      <section aria-labelledby="tonswap-portfolio-title">
        <div className="tonswap-toolbar">
          <div>
            <h2 id="tonswap-portfolio-title">{t('tonswap.portfolio.title')}</h2>
            <p>{t('tonswap.portfolio.hint')}</p>
          </div>
        </div>
        <div className="tonswap-portfolio-grid">
          <div className="tonswap-portfolio-main">
            <form className="card tonswap-wallet-form" onSubmit={event => { event.preventDefault(); lookup(address); }}>
              <label htmlFor="tonswap-portfolio-address">{t('tonswap.address')}</label>
              <div className="tonswap-wallet-input-row">
                <input id="tonswap-portfolio-address" dir="ltr" value={address} onChange={event => setAddress(event.target.value)}
                  maxLength={96} placeholder="0:… / kQ…" spellCheck={false} autoComplete="off" autoCapitalize="off"
                  aria-invalid={result.invalid || undefined} aria-describedby={result.invalid ? 'tonswap-portfolio-invalid' : undefined}/>
                <button className="btn primary" type="submit">{t('tonswap.portfolio.lookup')}</button>
              </div>
              <button className="tonswap-example" type="button" onClick={() => openWallet(BALANCE_EXAMPLE)}>{t('tonswap.example')}</button>
              {result.invalid && <p className="tonswap-wallet-error" id="tonswap-portfolio-invalid" role="alert">{t('tonswap.invalidAddress')}</p>}
            </form>
            {result.address && (
              <div className="card tonswap-portfolio-result">
                <div className="tonswap-follow-card">
                  <div>
                    <h3>{watched?.label || t('tonswap.portfolio.addressTitle')}</h3>
                    <TonAddressLink address={result.address}/>
                  </div>
                  <div className="tonswap-follow-actions">
                    {!watched && <label>{t('tonswap.portfolio.alias')}
                      <input value={alias} onChange={event => setAlias(event.target.value)} maxLength={40}/>
                    </label>}
                    <button type="button" className="btn" onClick={toggleFollow}>
                      {t(watched ? 'tonswap.portfolio.unfollow' : 'tonswap.portfolio.follow')}
                    </button>
                  </div>
                  {saveError && <p className="tonswap-wallet-error" role="alert">{t('tonswap.portfolio.saveError')}</p>}
                </div>
                <TonWalletPanels key={result.address} result={result} cache={historyCache.current}
                  onRefresh={() => lookup(result.address)}/>
              </div>
            )}
          </div>
          <TonWatchlist entries={watchlist} active={result.address} onOpen={openWallet}/>
        </div>
      </section>
    );
  }

  async function readT3Summary(release, signal) {
    const getter = method => readJson(INDEXER + '/runGetMethod', signal, { address: release.address, method, stack: [] });
    const [economy, ledger] = await Promise.all([getter('get_hub_state'), getter('get_jetton_ledger')]);
    const { amounts, supply } = TonswapProtocol.t3Backing(economy);
    const roots = TonswapProtocol.t3ReserveMasters(ledger);
    const reserves = amounts.map((amount, slot) => {
      const token = release.tokens.find(item => canonicalTonAddress(item.root) === roots[slot]);
      if (token && /^[A-Z0-9]{1,12}$/.test(token.symbol) && validDecimals(token.decimals)) {
        return { symbol: token.symbol, decimals: token.decimals, amount };
      }
      if (amount > 0n) throw new Error('Unknown T3 reserve');
      return null;
    });
    if (!['USDT', 'USDC'].every(symbol => reserves.some(token => token?.symbol === symbol))) {
      throw new Error('Missing T3 reserve metadata');
    }
    return { reserves, supply };
  }

  async function readTonAccountPage(address, cursor, signal, limit = 50) {
    const query = new URLSearchParams({ limit: String(limit) });
    if (cursor) query.set('before_lt', String(cursor.lt));
    const url = TON_BLOCKCHAIN + '/accounts/' + encodeURIComponent(address) + '/transactions?' + query;
    const { transactions } = await readJson(url, signal);
    if (!Array.isArray(transactions) || transactions.some(tx => tx?.account?.address !== address)) {
      throw new Error('Unexpected TON history');
    }
    let previous = cursor?.previous;
    let incomplete = transactions.length === 0;
    for (const [index, tx] of transactions.entries()) {
      if (index > 0 && !previous) incomplete = true;
      if (previous && (previous.lt !== tx.lt || previous.hash !== transactionHash(tx.hash))) incomplete = true;
      const hash = transactionHash(tx.prev_trans_hash);
      previous = hash && Number.isSafeInteger(tx.prev_trans_lt) && tx.prev_trans_lt > 0 &&
        tx.prev_trans_lt < tx.lt ? { lt: tx.prev_trans_lt, hash } : null;
    }
    const last = transactions.at(-1);
    const next = previous && Number.isSafeInteger(last.lt) && (!cursor || last.lt < cursor.lt)
      ? { lt: last.lt, previous } : null;
    const reachedDeployment = last?.orig_status === 'nonexist' && !last.prev_trans_lt;
    return { transactions, next, incomplete: incomplete || (!next && !reachedDeployment) };
  }

  function walletActivitySources(manifest) {
    const pools = marketCatalog(manifest).map(market => ({
      address: market.pool.toLowerCase(), market, rows: [],
    }));
    const faucet = manifest.contracts?.TestnetFaucet;
    const tokens = new Map(releaseTokens(manifest)
      .filter(token => RAW_ADDRESS.test(token?.root) && validDecimals(token.decimals))
      .map(token => [token.symbol, token.decimals]));
    return [...pools, {
      address: faucet?.codeHash === FAUCET_HASH && RAW_ADDRESS.test(faucet.address) ? faucet.address.toLowerCase() : null,
      tokens, rows: [],
    }];
  }

  async function readWalletHistoryData(cache, key, fresh, signal, read, maxAge = WALLET_HISTORY_CACHE_MS) {
    const entry = cache.get(key);
    if (!fresh && entry?.expiresAt > Date.now()) return entry.value;
    let value;
    try {
      value = await read();
    } catch (error) {
      if (error.status !== 429 || signal.aborted) throw error;
      const wait = AbortSignal.any([signal, AbortSignal.timeout(4500)]);
      await new Promise(resolve => wait.addEventListener('abort', resolve, { once: true }));
      signal.throwIfAborted();
      value = await read();
    }
    if (!signal.aborted && value && !value.incomplete) {
      cache.delete(key);
      if (cache.size >= WALLET_HISTORY_CACHE_SIZE) cache.delete(cache.keys().next().value);
      cache.set(key, { value, expiresAt: Date.now() + maxAge });
    }
    return value;
  }

  async function readWalletActivity(source, wallet, cache, fresh, signal) {
    if (!source.address) throw new Error('Unsupported wallet activity source');
    const key = 'page:' + source.address + ':' +
      (source.next ? source.next.lt + ':' + source.next.previous.hash : 'head');
    // The same contract pages serve every wallet. Refresh still checks the chain head.
    const { transactions, next, incomplete } = await readWalletHistoryData(cache, key, fresh, signal,
      () => readTonAccountPage(source.address, source.next, signal));
    let rows;
    let partial = incomplete;
    if (source.market) {
      const decoded = TonswapProtocol.walletPoolEvents(transactions, source.address, wallet, decodeTonswapLiquidity);
      rows = decoded.rows.map(row => ({ ...row, market: source.market }));
      partial ||= decoded.incomplete;
    } else {
      rows = [];
      for (const tx of transactions) {
        if (signal.aborted) break;
        try {
          const grant = TonswapProtocol.faucetGrant(tx, source.address, wallet);
          if (!grant) continue;
          const hash = transactionHash(grant.claimHash);
          const decimals = source.tokens.get(grant.symbol);
          if (!hash || !validDecimals(decimals)) throw new Error('Unknown faucet token');
          const credit = await readWalletHistoryData(cache, 'credit:' + wallet + ':' + hash, false, signal, async () => {
            const trace = await readJson(TON_TRACES + hash, signal);
            return TonswapProtocol.faucetCredit(trace, grant, source.address, wallet);
          }, WALLET_CREDIT_CACHE_MS);
          if (credit) rows.push({ ...credit, decimals });
        } catch {
          partial = true;
        }
      }
    }
    const merged = new Map(source.rows.map(row => [row.hash + ':' + row.index, row]));
    for (const row of rows) {
      const hash = transactionHash(row.hash);
      if (!hash || !validTonTime(row.utime)) {
        partial = true;
        continue;
      }
      merged.set(hash + ':' + row.index, { ...row, hash });
    }
    return { ...source, rows: [...merged.values()], next, incomplete: source.incomplete || partial, failed: false };
  }

  function useWalletActivity(wallet, cache) {
    const [request, setRequest] = useState({ sources: null });
    const [data, setData] = useState({ loading: true, sources: [] });
    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
      async function load() {
        try {
          const sources = request.sources || walletActivitySources(await readJson(MANIFEST, signal));
          const results = await Promise.allSettled(sources.map(source =>
            !request.sources || source.next || source.failed
              ? readWalletActivity(source, wallet, cache, request.fresh, signal) : source));
          if (!signal.aborted) setData({
            sources: results.map((result, i) => result.status === 'fulfilled'
              ? result.value : { ...sources[i], failed: true }),
          });
        } catch {
          if (!signal.aborted) setData(previous => ({ ...previous, loading: false, error: true }));
        }
      }
      load();
      return () => controller.abort();
    }, [wallet, cache, request]);

    function read(sources = null, fresh = false) {
      setData(previous => ({ ...previous, loading: true, error: false, sources: sources || [] }));
      setRequest({ sources, fresh });
    }
    const refresh = () => read(null, true);
    const error = data.error || (data.sources.length > 0 && data.sources.every(source => source.failed));
    return {
      ...data, refresh, retry: refresh, loadMore: () => read(data.sources),
      error,
      incomplete: !error && data.sources.some(source => source.incomplete || source.failed),
      next: data.sources.some(source => source.next),
    };
  }

  function TonWalletHistoryAmount({ symbol, amount, decimals, credit }) {
    const { locale } = useLang();
    return <bdi className={'tonswap-wallet-history-amount' + (credit ? ' is-credit' : '')}>
      <TonTokenIcon symbol={symbol}/><span className="num">{credit && '+'}{formatTokenAmount(amount, decimals, locale)}</span>{' '}
      <strong>{symbol}</strong>
    </bdi>;
  }

  function TonWalletHistoryTable({ rows, wallet }) {
    const { t, locale } = useLang();
    const dateFormat = new Intl.DateTimeFormat(locale, { dateStyle: 'medium' });
    const timeFormat = new Intl.DateTimeFormat(locale, { timeStyle: 'short' });
    return (
      <table className="lp-table tonswap-wallet-table tonswap-wallet-history-table">
        <thead><tr>
          <th scope="col">{t('s.date')}</th>
          <th scope="col">{t('s.detail2')}</th>
        </tr></thead>
        <tbody>
          {rows.map(row => {
            const date = new Date(row.utime * 1000);
            const { market } = row;
            const amount = (side, raw) => <TonWalletHistoryAmount symbol={side === 0 ? 'T3' : market.symbol}
              amount={raw} decimals={side === 0 ? market.t3Decimals : market.xDecimals}/>;
            const isOwner = row.owner === wallet;
            const counterparty = row.payer && row.owner !== row.payer ? (isOwner ? row.payer : row.owner) : null;
            return (
              <tr key={row.hash + ':' + row.index}>
                <td>
                  <time dateTime={date.toISOString()}>
                    <span>{dateFormat.format(date)}</span>
                    <small className="muted">{timeFormat.format(date)}</small>
                  </time>
                </td>
                <td>
                  <div className="tonswap-wallet-history-row-head">
                    <strong>{t('tonswap.walletHistory.action.' + row.kind)}</strong>
                    <TonTxLink hash={row.hash} label="tonswap.walletHistory.tx" accessibleLabel="tonswap.viewTx"/>
                  </div>
                  <div className="tonswap-wallet-history-values">
                    {row.kind === 'swap' ? <>
                      {amount(row.side, row.amountIn)}
                      <span className="muted" role="img" aria-label={t('tonswap.walletHistory.for')}>→</span>
                      {amount(1 - row.side, row.amountOut)}
                    </> : market ? <>
                      {row.t3 > 0n && amount(0, row.t3)}
                      {row.asset > 0n && amount(1, row.asset)}
                      <span className="muted">{market.symbol} / T3</span>
                    </> : <TonWalletHistoryAmount {...row} credit/>}
                  </div>
                  {counterparty && <div className="muted tiny tonswap-wallet-history-party">
                    {t(isOwner ? 'tonswap.activity.payer' : 'tonswap.activity.owner')} <TonAddressLink address={counterparty}/>
                  </div>}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    );
  }

  function TonWalletHistory({ wallet, cache }) {
    const t = useT();
    const history = useWalletActivity(wallet, cache);
    const [filter, setFilter] = useState('all');
    const rows = history.sources.flatMap(source => source.rows)
      .sort((a, b) => b.utime - a.utime || a.hash.localeCompare(b.hash));
    const categories = {
      all: rows,
      swap: rows.filter(row => row.kind === 'swap'),
      liquidity: rows.filter(row => row.market && row.kind !== 'swap'),
      faucet: rows.filter(row => row.kind === 'faucet'),
    };
    const grants = new Map();
    if (filter === 'faucet') {
      for (const row of categories.faucet) {
        grants.set(row.symbol, { ...row, amount: row.amount + (grants.get(row.symbol)?.amount ?? 0n) });
      }
    }
    return (
      <div className="tonswap-wallet-history">
        <div className="tonswap-wallet-toolbar">
          <p className="muted tiny">{t('tonswap.walletHistory.scope')}</p>
          <div>
            <select className="sm-input" aria-label={t('tonswap.walletHistory.filter')}
              value={filter} onChange={event => setFilter(event.target.value)}>
              {Object.keys(categories).map(kind => <option value={kind} key={kind}>
                {t('tonswap.walletHistory.filter.' + kind)}
              </option>)}
            </select>
            <button className="btn" type="button" onClick={history.refresh} disabled={history.loading}>{t('tonswap.refresh')}</button>
          </div>
        </div>
        {grants.size > 0 && (
          <div className="tonswap-wallet-history-total">
            <span className="muted tiny">{t('tonswap.walletHistory.received')}</span>
            {[...grants.values()].map(row => <TonWalletHistoryAmount key={row.symbol} {...row} credit/>)}
          </div>
        )}
        <ProtocolActivity history={history} rows={categories[filter]} empty={'tonswap.walletHistory.empty.' + filter}
          renderRows={visibleRows => <TonWalletHistoryTable rows={visibleRows} wallet={wallet}/>}/>
      </div>
    );
  }

  async function readTonProtocolHistory(kind, { address, factory }, cursor, signal) {
    const page = await readTonAccountPage(address, cursor, signal);
    const { transactions } = page;
    const decoded = kind === 't3' ? TonswapProtocol.t3Events(transactions, address) :
      TonswapProtocol.optionEvents(transactions, address, factory);
    return { rows: decoded.rows, next: page.next, incomplete: page.incomplete || decoded.incomplete };
  }

  async function readSpotOrders({ address }, cursor, signal) {
    const page = await readTonAccountPage(address, cursor, signal, 20);
    const { orders, incomplete } = TonswapProtocol.spotOrderRequests(page.transactions, address, canonicalTonAddress);
    if (!orders.length) return { rows: [], next: page.next, incomplete: incomplete || page.incomplete };
    const { accounts } = await readJson(TON_BLOCKCHAIN + '/accounts/_bulk', signal, { account_ids: orders.map(order => order.order) });
    if (!Array.isArray(accounts)) throw new Error('Unexpected order accounts');
    const byAddress = new Map(accounts.map(account => [account.address, account]));
    const hashes = new Map();
    const results = await Promise.allSettled(orders.map(async order => {
      const account = byAddress.get(order.order);
      if (account?.status !== 'active' || !Number.isSafeInteger(account.last_transaction_lt) || account.last_transaction_lt <= order.lt) {
        throw new Error('Order state unavailable');
      }
      if (!hashes.has(account.code)) hashes.set(account.code, TonswapProtocol.spotOrderCodeHash(account.code));
      if (await hashes.get(account.code) !== SPOT_ORDER_HASH) throw new Error('Unexpected order code');
      return { ...order, ...TonswapProtocol.spotOrderState(account.data, address, order) };
    }));
    const rows = results.flatMap(result => result.status === 'fulfilled' ? [result.value] : []);
    return { rows, next: page.next, incomplete: page.incomplete || incomplete || rows.length !== orders.length };
  }

  async function readProtocolHistory(kind, release, cursor, signal) {
    if (kind === 'orders') return readSpotOrders(release, cursor, signal);
    if (kind === 't3' || kind === 'options') return readTonProtocolHistory(kind, release, cursor, signal);
    const query = new URLSearchParams(cursor ? { cursor_lt: cursor.lt, cursor_hash: cursor.hash } : { page: '1' });
    const url = INDEXER + '/accounts/' + encodeURIComponent(release.address) + '/txs?' + query;
    const page = await readJson(url, signal);
    if (page.network !== 'testnet' || !Array.isArray(page.txs) ||
        !Number.isInteger(page.page_size) || page.page_size < 1) throw new Error('Unexpected contract history');
    const decoded = TonswapProtocol.contractEvents(page.txs, kind, release.address, canonicalTonAddress);
    const more = page.txs.length >= page.page_size &&
      (cursor || (page.total_pages ?? page.total_pages_min) > 1 || page.history_complete !== true);
    const last = page.txs.at(-1);
    const next = more && /^\d+$/.test(last?.lt) && transactionHash(last.hash) && last.lt !== cursor?.lt
      ? { lt: last.lt, hash: last.hash } : null;
    return { ...decoded, next, incomplete: page.history_complete !== true || decoded.incomplete || Boolean(more && !next) };
  }

  function optionCatalog(manifest) {
    const factory = canonicalTonAddress(manifest.contracts?.OptionFactory?.address);
    if (!factory || manifest.contracts.OptionFactory.codeHash !== OPTION_FACTORY_HASH || !Array.isArray(manifest.options)) {
      throw new Error('Unexpected options catalog');
    }
    const series = [];
    const templates = [];
    for (const { kind, symbol, comparisonSymbol, option, seriesId, expiry } of manifest.options) {
      if (!['shout', 'outperformance'].includes(kind) || !/^[A-Z0-9]{1,12}$/.test(symbol) ||
          (kind === 'outperformance' && !/^[A-Z0-9]{1,12}$/.test(comparisonSymbol))) {
        throw new Error('Unexpected option market');
      }
      const market = symbol + ' / ' + (kind === 'shout' ? 'T3' : comparisonSymbol);
      if (option == null && seriesId == null && expiry == null) {
        templates.push({ kind, market });
        continue;
      }
      const address = canonicalTonAddress(option);
      const end = Number(expiry);
      if (!address || typeof seriesId !== 'string' || !/^[1-9]\d{0,19}$/.test(seriesId) ||
          BigInt(seriesId) > 0xffffffffffffffffn || !Number.isInteger(end) || end <= 0 || end > 0xffffffff ||
          series.some(item => item.seriesId === seriesId)) {
        throw new Error('Unexpected option series');
      }
      series.push({ kind, symbol, market, address, seriesId, expiry: end });
    }
    return { factory, series, templates };
  }

  async function readOptionSeries(series, factory, signal) {
    const getter = (address, method, stack = []) => readJson(INDEXER + '/runGetMethod', signal, { address, method, stack });
    const [terms, capacity] = await Promise.allSettled([
      getter(factory, 'series_info', [['num', series.seriesId]]).then(TonswapProtocol.optionSeriesTerms),
      getter(series.address, 'get_capacity').then(TonswapProtocol.optionCapacity),
    ]);
    const verified = terms.status === 'fulfilled' && terms.value.address === series.address &&
      terms.value.kind === series.kind && terms.value.expiry === series.expiry;
    return { ...series, verified, capacity: verified && capacity.status === 'fulfilled' ? capacity.value : null };
  }

  function useProtocolHistory(kind) {
    const [request, setRequest] = useState({ cursor: null, release: null });
    const [data, setData] = useState({ rows: [], loading: true, summaryLoading: kind === 't3' });
    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
      const { cursor } = request;
      async function load() {
        let summaryStarted = false;
        try {
          let release = request.release;
          if (!release) {
            const manifest = await readJson(MANIFEST, signal);
            const tokens = releaseTokens(manifest);
            const [name, codeHash] = PROTOCOL_CONTRACTS[kind];
            const contract = manifest.contracts?.[name];
            const address = canonicalTonAddress(contract?.address);
            if (!address || contract.codeHash !== codeHash) throw new Error('Unsupported Tonswap contract version');
            const markets = kind === 'perps' ? manifest.perpsMarkets.map(({ marketId, pool, symbol }) => {
              if (!Number.isInteger(marketId) || marketId < 0 || marketId > 0xffffffff ||
                  !RAW_ADDRESS.test(pool) || !/^[A-Z0-9]{1,12}$/.test(symbol)) {
                throw new Error('Unexpected Perps market');
              }
              return { marketId, pool, symbol };
            }) : [];
            release = {
              address, tokens, markets, minterCode: manifest.templates?.JettonV21Minter,
              ...(kind === 'options' ? optionCatalog(manifest) : {}),
              ...(kind === 'orders' ? { spotMarkets: marketCatalog(manifest) } : {}),
            };
            if (kind === 'orders' && manifest.templates?.SpotOrder !== SPOT_ORDER_HASH) throw new Error('Unsupported order template');
            if (signal.aborted) return;
            setData(previous => ({ ...previous, release }));
          }
          if (kind === 't3' && !cursor) {
            summaryStarted = true;
            readT3Summary(release, signal).then(
              summary => { if (!signal.aborted) setData(previous => ({ ...previous, summary, summaryLoading: false })); },
              () => { if (!signal.aborted) setData(previous => ({ ...previous, summaryError: true, summaryLoading: false })); },
            );
          }
          const seriesRequest = request.release ? release.series : Promise.all(
            (release.series || []).map(series => readOptionSeries(series, release.factory, signal))
          ).then(series => {
            if (kind === 'options' && !signal.aborted) setData(previous => ({ ...previous, release: { ...release, series } }));
            return series;
          });
          const [decoded, markets, series] = await Promise.all([
            readProtocolHistory(kind, release, cursor, signal),
            request.release ? release.markets : Promise.all(release.markets.map(async market => {
              try {
                const result = await readJson(INDEXER + '/runGetMethod', signal, {
                  address: release.address, method: 'get_market', stack: [['num', String(market.marketId)]],
                });
                return { ...market, stats: TonswapProtocol.perpsMarket(result, market.pool) };
              } catch {
                return market;
              }
            })),
            seriesRequest,
          ]);
          const rows = decoded.rows.map(row => ({ ...row, hash: transactionHash(row.hash) }))
            .filter(row => row.hash && validTonTime(row.utime));
          const incomplete = decoded.incomplete || rows.length !== decoded.rows.length;
          if (signal.aborted) return;
          setData(previous => {
            const all = cursor ? previous.rows.concat(rows) : rows;
            const unique = new Map(all.map(row => [row.order || row.hash + ':' + row.index, row]));
            return {
              ...previous, loading: false, error: false,
              release: { ...release, markets, series }, rows: [...unique.values()], next: decoded.next,
              incomplete: incomplete || Boolean(cursor && previous.incomplete),
            };
          });
        } catch {
          if (!signal.aborted) setData(previous => ({
            ...previous, loading: false, error: true,
            ...(!summaryStarted && !cursor && kind === 't3' ? { summaryLoading: false, summaryError: true } : {}),
          }));
        }
      }
      load();
      return () => controller.abort();
    }, [kind, request]);

    function read(cursor = null) {
      setData(previous => ({
        ...previous, loading: true, error: false,
        ...(!cursor && kind === 't3' ? { summaryLoading: true, summaryError: false } : {}),
      }));
      setRequest({ cursor, release: cursor ? data.release : null });
    }
    return { ...data, refresh: () => read(), retry: () => read(request.cursor), loadMore: () => read(data.next) };
  }

  function validTonTime(value) {
    return Number.isSafeInteger(value) && value > 0 && value < 253402300800;
  }

  function TonTxLink({ hash, label = 'tonswap.viewTx', accessibleLabel }) {
    const t = useT();
    return <a href={EXPLORER + '/tx/' + hash} target="_blank" rel="noopener noreferrer"
      aria-label={accessibleLabel ? t(accessibleLabel) : undefined}>
      {t(label)} <span aria-hidden="true">↗</span>
    </a>;
  }

  function TonEventDate({ seconds }) {
    const { locale } = useLang();
    const date = new Date(seconds * 1000);
    return <time dateTime={date.toISOString()}>{new Intl.DateTimeFormat(locale, {
      dateStyle: 'medium', timeStyle: 'short',
    }).format(date)}</time>;
  }

  function ProtocolToolbar({ kind, history, contractLabel = 'tonswap.viewContract' }) {
    const t = useT();
    return (
      <header className="tonswap-toolbar">
        <div>
          <h2 id={'tonswap-' + kind + '-title'}>{t('tonswap.' + kind + '.title')}</h2>
          <p>{t('tonswap.' + kind + '.description')}</p>
        </div>
        <div className="tonswap-toolbar-actions">
          {history.release && <TonAddressLink address={history.release.address} label={t(contractLabel)}/>}
          <button className="btn" type="button" onClick={history.refresh} disabled={history.loading || history.summaryLoading}>{t('tonswap.refresh')}</button>
        </div>
      </header>
    );
  }

  function ProtocolActivity({ history, rows, empty, children, renderRows }) {
    const t = useT();
    return (
      <div aria-busy={Boolean(history.loading)}>
        {history.loading && <p className="tonswap-note" role="status">{t('tonswap.protocol.loading')}</p>}
        {history.error && <div className="tonswap-protocol-message">
          <p className="tonswap-note" role="alert">{t('tonswap.protocol.error')}</p>
          <button type="button" className="btn" onClick={history.retry}>{t('common.retry')}</button>
        </div>}
        {history.incomplete && <p className="tonswap-note" role="status">{t('tonswap.protocol.partial')}</p>}
        {!history.loading && !history.error && !rows.length && <div className="tonswap-protocol-empty" role="status">
          {t(history.incomplete || history.next ? 'tonswap.protocol.emptyPage' : empty)}
        </div>}
        {!!rows.length && (renderRows ? renderRows(rows) : <ol className="tonswap-protocol-list" role="list">
          {rows.map(row => <li className="tonswap-protocol-row" key={row.hash + ':' + row.index}>{children(row)}</li>)}
        </ol>)}
        {history.next && !history.error && <button className="btn tonswap-protocol-more" type="button"
          disabled={history.loading || history.summaryLoading} onClick={history.loadMore}>{t('tonswap.activity.more')}</button>}
      </div>
    );
  }

  function PerpsView() {
    const { t, locale } = useLang();
    const history = useProtocolHistory('perps');
    const [filter, setFilter] = useState(null);
    const actions = ['open', 'close', 'liquidate'];
    const markets = history.release?.markets || [];
    const marketLabel = id => markets.find(market => market.marketId === id)?.symbol;
    const rows = history.rows.filter(row => !filter || row.kind === filter);
    const complete = history.release && !history.loading && !history.error && !history.incomplete && !history.next;
    const amount = value => formatTokenAmount(value < 0n ? -value : value, 9, locale);
    return (
      <section aria-labelledby="tonswap-perps-title">
        <ProtocolToolbar kind="perps" history={history}/>
        <div className="tonswap-protocol-markets">
          {markets.map(market => <article className="card tonswap-protocol-market" key={market.marketId}>
            <div className="tonswap-pair">
              <TonTokenIcon symbol={market.symbol}/>
              <div><small>{t('tonswap.perps.market')}</small><h3>{market.symbol} / T3</h3></div>
            </div>
            <dl>
              <div><dt>{t('tonswap.perps.longOi')}</dt><dd>{market.stats ? amount(market.stats.long) + ' T3' : '—'}</dd></div>
              <div><dt>{t('tonswap.perps.shortOi')}</dt><dd>{market.stats ? amount(market.stats.short) + ' T3' : '—'}</dd></div>
              <div><dt>{t('tonswap.perps.cap')}</dt><dd>{market.stats ? amount(market.stats.cap) + ' T3' : '—'}</dd></div>
              <div><dt>{t('tonswap.perps.leverage')}</dt><dd>{market.stats ? fmtNumber(market.stats.leverage, locale) + '×' : '—'}</dd></div>
            </dl>
          </article>)}
        </div>
        <div className="tonswap-protocol-stats" role="group" aria-label={t('tonswap.perps.filters')}>
          {actions.map(action => {
            const count = history.rows.filter(row => row.kind === action).length;
            return <button className="card tonswap-protocol-stat" data-action={action} type="button"
              aria-pressed={filter === action} key={action} onClick={() => setFilter(filter === action ? null : action)}>
              <span>{t('tonswap.perps.' + action + 'Count')}</span>
              <strong>{complete ? fmtNumber(count, locale) : count ? '≥ ' + fmtNumber(count, locale) : '—'}</strong>
            </button>;
          })}
        </div>
        <article className="card tonswap-protocol-feed">
          <header><h3>{t('tonswap.activity.title')}</h3>
            <button className="btn" type="button" aria-pressed={!filter} onClick={() => setFilter(null)}>{t('tonswap.protocol.all')}</button>
          </header>
          <ProtocolActivity history={history} rows={rows} empty={filter ? 'tonswap.perps.emptyFilter' : 'tonswap.perps.empty'}>
            {row => <>
              <div><strong>{t(row.kind === 'close' && row.size !== 0n ? 'tonswap.perps.partialClose' : 'tonswap.perps.' + row.kind)}</strong>
                <TonEventDate seconds={row.utime}/></div>
              <div>
                <strong>{marketLabel(row.marketId) ? marketLabel(row.marketId) + ' / T3' : '#' + row.marketId}</strong>
                <span>{row.size === 0n ? t('tonswap.perps.noPosition') :
                  t('tonswap.perps.position').replace('{size}', amount(row.size))
                    .replace('{side}', t(row.size > 0n ? 'tonswap.perps.long' : 'tonswap.perps.short'))}</span>
              </div>
              <div>
                <small>{t('tonswap.activity.owner')}</small><TonAddressLink address={row.owner}/>
              </div>
              <details><summary>{t('tonswap.protocol.details')}</summary>
                {row.fill > 0n && <span>{t('tonswap.perps.fill')} <bdi>{amount(row.fill)} T3</bdi></span>}
                <span>{t('tonswap.perps.margin')} <bdi>{row.margin < 0n ? '−' : row.margin > 0n ? '+' : ''}{amount(row.margin)} T3</bdi></span>
                <span>{t('tonswap.fee')} <bdi>{amount(row.fee)} T3</bdi></span>
              </details>
              <TonTxLink hash={row.hash}/>
            </>}
          </ProtocolActivity>
        </article>
      </section>
    );
  }

  async function creatorPage(creator, release, cursor, signal) {
    if (!/^[a-f0-9]{64}$/.test(release.minterCode)) throw new Error('Missing Launchpad minter template');
    const params = new URLSearchParams({ limit: String(CREATOR_PAGE_SIZE) });
    if (cursor) params.set('before_lt', cursor);
    const { transactions } = await readJson(TON_BLOCKCHAIN + '/accounts/' + encodeURIComponent(creator) + '/transactions?' + params, signal);
    if (!Array.isArray(transactions)) throw new Error('Unexpected wallet history');
    const published = new Map(release.tokens.map(token => [canonicalTonAddress(token.root), token.symbol]));
    const rows = [];
    let incomplete = false;
    for (const tx of transactions) {
      if (!Array.isArray(tx.out_msgs)) {
        incomplete = true;
        continue;
      }
      if (!tx.success || tx.aborted) continue;
      for (const message of tx.out_msgs) {
        if (message.op_code !== '0x642b7d07' || !message.init || message.bounced) continue;
        const token = canonicalTonAddress(message.destination?.address);
        const hash = transactionHash(message.hash);
        if (!token || !hash) continue;
        try {
          const encoded = encodeURIComponent(token);
          const deployment = await readJson(TON_BLOCKCHAIN + '/messages/' + hash + '/transaction', signal);
          if (!deployment.success || deployment.aborted || deployment.end_status !== 'active' ||
              !['nonexist', 'uninit'].includes(deployment.orig_status) ||
              canonicalTonAddress(deployment.account?.address) !== token ||
              canonicalTonAddress(deployment.in_msg?.source?.address) !== creator ||
              transactionHash(deployment.in_msg?.hash) !== hash ||
              deployment.in_msg?.op_code !== '0x642b7d07' || !deployment.in_msg?.init ||
              !validTonTime(deployment.utime)) continue;
          const txHash = transactionHash(deployment.hash);
          if (!txHash) throw new Error('Missing deployment hash');
          const init = await TonswapProtocol.stateInitHashes(deployment.in_msg.init.boc);
          if (!init || init.code !== release.minterCode || init.root !== token.split(':')[1]) continue;
          const info = published.has(token) ? null :
            await readJson('https://testnet.tonapi.io/v2/jettons/' + encoded, signal).catch(() => null);
          const metadata = info?.metadata;
          const name = [published.get(token), metadata?.symbol, metadata?.name].find(value => typeof value === 'string' && value.trim());
          rows.push({ token, name: name?.trim().slice(0, 80), hash: txHash, utime: deployment.utime, releaseToken: published.has(token) });
        } catch {
          if (signal.aborted) throw new Error('Token lookup cancelled');
          incomplete = true;
        }
      }
    }
    const last = transactions.at(-1)?.lt;
    const next = transactions.length === CREATOR_PAGE_SIZE && Number.isSafeInteger(last) && last > 0 &&
      String(last) !== cursor ? String(last) : null;
    return { rows, next, incomplete: incomplete || (transactions.length === CREATOR_PAGE_SIZE && !next) };
  }

  function CreatorTokens({ release }) {
    const t = useT();
    const request = useRef(null);
    const [input, setInput] = useState('');
    const [data, setData] = useState({ rows: [] });
    useEffect(() => () => request.current?.abort(), []);
    async function lookup(value, cursor = null) {
      request.current?.abort();
      const creator = canonicalTonAddress(value);
      if (!creator) {
        setData({ rows: [], invalid: true });
        return;
      }
      const controller = new AbortController();
      request.current = controller;
      const { signal } = controller;
      setData(previous => ({ creator, rows: cursor ? previous.rows : [], loading: true }));
      try {
        const page = await creatorPage(creator, release, cursor, signal);
        if (!signal.aborted) setData(previous => ({
          ...page, creator, incomplete: page.incomplete || Boolean(cursor && previous.incomplete),
          rows: [...new Map(previous.rows.concat(page.rows).map(row => [row.token, row])).values()],
        }));
      } catch {
        if (!signal.aborted) setData(previous => ({ ...previous, loading: false, error: true, cursor }));
      }
    }
    return (
      <article className="card tonswap-protocol-feed tonswap-creator">
        <header><div><h3>{t('tonswap.launchpad.tokens')}</h3><p>{t('tonswap.launchpad.tokenScope')}</p></div></header>
        <form className="tonswap-wallet-form" onSubmit={event => { event.preventDefault(); lookup(input); }}>
          <label htmlFor="tonswap-creator">{t('tonswap.launchpad.creator')}</label>
          <div className="tonswap-wallet-input-row">
            <input id="tonswap-creator" dir="ltr" value={input} onChange={event => setInput(event.target.value)}
              maxLength={96} placeholder="0:… / kQ…" spellCheck={false} autoComplete="off" autoCapitalize="off"
              aria-invalid={data.invalid || undefined}/>
            <button className="btn primary" type="submit" disabled={!release}>{t('tonswap.launchpad.lookup')}</button>
          </div>
          <button className="tonswap-example" type="button" disabled={!release} onClick={() => {
            setInput(CREATOR_EXAMPLE);
            lookup(CREATOR_EXAMPLE);
          }}>{t('tonswap.launchpad.example')}</button>
          {data.invalid && <p className="tonswap-wallet-error" role="alert">{t('tonswap.invalidAddress')}</p>}
        </form>
        {data.creator && <ProtocolActivity
          history={{ ...data, retry: () => lookup(data.creator, data.cursor), loadMore: () => lookup(data.creator, data.next) }}
          rows={data.rows} empty="tonswap.launchpad.emptyTokens">
          {row => <>
            <div>
              <TonAddressLink address={row.token} label={row.name || t('tonswap.launchpad.token')}/>
              {row.releaseToken && <small>{t('tonswap.launchpad.releaseToken')}</small>}
              <TonEventDate seconds={row.utime}/>
            </div>
            <div><small>{t('tonswap.launchpad.creator')}</small><TonAddressLink address={data.creator}/></div>
            <TonTxLink hash={row.hash}/>
          </>}
        </ProtocolActivity>}
      </article>
    );
  }

  function LaunchpadView() {
    const { t, locale } = useLang();
    const history = useProtocolHistory('launchpad');
    const tokenLabel = root => history.release?.tokens.find(token => canonicalTonAddress(token.root) === root)?.symbol;
    return (
      <section aria-labelledby="tonswap-launchpad-title">
        <ProtocolToolbar kind="launchpad" history={history}/>
        <div className="tonswap-launchpad-grid">
          <article className="card tonswap-protocol-feed">
            <header><div><h3>{t('tonswap.launchpad.sales')}</h3><p>{t('tonswap.launchpad.salesScope')}</p></div></header>
            <ProtocolActivity history={history} rows={history.rows} empty="tonswap.launchpad.emptySales">
              {row => <>
                <div><strong>{t('tonswap.launchpad.' + row.kind)}</strong><TonEventDate seconds={row.utime}/></div>
                <div><small>{t('tonswap.launchpad.token')}</small><TonAddressLink address={row.token} label={tokenLabel(row.token)}/></div>
                <div><small>{t('tonswap.launchpad.creator')}</small><TonAddressLink address={row.owner}/></div>
                <details><summary>{t('tonswap.protocol.details')}</summary>
                  <TonAddressLink address={row.sale} label={t('tonswap.launchpad.saleContract')}/>
                  <span>{t('tonswap.launchpad.supply')} <bdi>{formatTokenAmount(row.supply, row.decimals, locale)}</bdi></span>
                  <span>{t('tonswap.launchpad.hardCap')} <bdi>{formatTokenAmount(row.hardCap, 9, locale)} T3</bdi></span>
                  <span>{t('tonswap.launchpad.starts')} <TonEventDate seconds={row.starts}/></span>
                  <span>{t('tonswap.launchpad.ends')} <TonEventDate seconds={row.ends}/></span>
                </details>
                <TonTxLink hash={row.hash}/>
              </>}
            </ProtocolActivity>
          </article>
          <CreatorTokens key={history.release?.address || 'pending'} release={history.release}/>
        </div>
      </section>
    );
  }

  function T3View() {
    const { t, locale } = useLang();
    const history = useProtocolHistory('t3');
    const [filter, setFilter] = useState('all');
    const rows = history.rows.filter(row => filter === 'all' || row.kind === filter);
    const summary = history.summary;
    const summaryLabel = token => token === 'T3' ? t('tonswap.t3.supply') : t('tonswap.t3.reserve').replace('{token}', token);
    const reserveTokens = ['USDT', 'USDC', ...(summary?.reserves.filter(token => token?.amount > 0n &&
      token.symbol !== 'USDT' && token.symbol !== 'USDC').map(token => token.symbol) || [])];
    const balanceCards = reserveTokens.map(symbol => ({ symbol, balance: summary?.reserves.find(token => token?.symbol === symbol) }));
    balanceCards.push({ symbol: 'T3', balance: summary && { amount: summary.supply, decimals: 9 } });

    function amountText(amount, decimals, symbol) {
      return formatTokenAmount(amount, decimals, locale) + ' ' + symbol;
    }

    return (
      <section aria-labelledby="tonswap-t3-title" className="tonswap-t3">
        <ProtocolToolbar kind="t3" history={history}/>
        <div className="tonswap-t3-balances" aria-busy={Boolean(history.summaryLoading)}>
          {balanceCards.map(({ symbol, balance }) => <article className="card tonswap-t3-balance" key={symbol}>
            <header><TonTokenIcon symbol={symbol}/><h3>{summaryLabel(symbol)}</h3></header>
            <strong>{balance ? amountText(balance.amount, balance.decimals, symbol) : '—'}</strong>
          </article>)}
        </div>
        {history.summaryError && <p className="tonswap-note" role="alert">{t('tonswap.t3.summaryError')}</p>}
        <article className="card tonswap-protocol-feed">
          <header>
            <div><h3>{t('tonswap.activity.title')}</h3><p>{t('tonswap.t3.activityNote')}</p></div>
            <div className="segmented" role="group" aria-label={t('tonswap.t3.filters')}>
              {['all', 'mint', 'redeem'].map(action => <button key={action} type="button"
                className={filter === action ? 'active' : ''} aria-pressed={filter === action}
                onClick={() => setFilter(action)}>{t(action === 'all' ? 'tonswap.protocol.all' : 'tonswap.t3.' + action + 'Filter')}</button>)}
            </div>
          </header>
          <ProtocolActivity history={history} rows={rows} empty={'tonswap.t3.empty.' + filter}>
            {row => <>
              <div>
                <strong>{t(row.paired ? 'tonswap.t3.pairedMint' : 'tonswap.t3.' + row.kind)}</strong>
                <TonEventDate seconds={row.utime}/>
              </div>
              <div>
                <strong>{amountText(row.amount, 9, 'T3')}</strong>
                {row.reserves?.map((amount, slot) => {
                  const token = summary?.reserves[slot];
                  return amount > 0n && token && <bdi key={slot}>{amountText(amount, token.decimals, token.symbol)}</bdi>;
                })}
              </div>
              <div>
                <small>{t('tonswap.t3.wallet')}</small><TonAddressLink address={row.owner}/>
                {row.recipient !== row.owner && <small>{t('tonswap.t3.recipient')} <TonAddressLink address={row.recipient}/></small>}
              </div>
              <TonTxLink hash={row.hash}/>
            </>}
          </ProtocolActivity>
        </article>
      </section>
    );
  }

  function OptionsView() {
    const { t, locale } = useLang();
    const history = useProtocolHistory('options');
    const [filter, setFilter] = useState('all');
    const [selection, setSelection] = useState('all');
    const series = history.release?.series || [];
    const templates = history.release?.templates || [];
    const selected = series.some(item => item.seriesId === selection) ? selection : 'all';
    const visible = history.rows.filter(row => selected === 'all' || row.seriesId === selected);
    const rows = visible.filter(row => filter === 'all' || row.kind === filter);
    const complete = !history.loading && !history.error && !history.incomplete && !history.next;
    const seriesLabel = id => t('tonswap.options.seriesId').replace('{id}', id);
    const seriesTitle = item => t('tonswap.options.' + item.kind) + ' · ' + item.market;

    function countLabel(kind, items) {
      const count = items.filter(row => row.kind === kind).length;
      if (!complete && count === 0) return '—';
      return (complete ? '' : '≥ ') + fmtNumber(count, locale);
    }

    return (
      <section aria-labelledby="tonswap-options-title" className="tonswap-options">
        <ProtocolToolbar kind="options" history={history} contractLabel="tonswap.options.vault"/>
        {series.length > 0 && <div className="tonswap-pool-grid tonswap-options-series">
          {series.map(item => {
            const activity = history.rows.filter(row => row.seriesId === item.seriesId);
            const status = item.verified === false ? 'unverified' :
              item.expiry * 1000 <= Date.now() ? 'expired' : 'unexpired';
            return (
              <article className="card tonswap-options-card" key={item.seriesId}>
                <header>
                  <div className="tonswap-pair">
                    <TonTokenIcon symbol={item.symbol}/>
                    <div><small>{seriesLabel(item.seriesId)}</small><h3>{seriesTitle(item)}</h3></div>
                  </div>
                  <span className={status === 'unexpired' ? 'tag info' : 'tag'}>{t('tonswap.options.' + status)}</span>
                </header>
                <dl>
                  <div><dt>{t('tonswap.options.expiry')}</dt><dd><TonEventDate seconds={item.expiry}/></dd></div>
                  <div><dt>{t('tonswap.options.positions')}</dt><dd>{item.capacity ?
                    fmtNumber(item.capacity.count, locale) + ' / ' + fmtNumber(item.capacity.limit, locale) : '—'}</dd></div>
                  <div><dt>{t('tonswap.options.buyFilter')}</dt><dd>{countLabel('buy', activity)}</dd></div>
                  <div><dt>{t('tonswap.options.exerciseFilter')}</dt><dd>{countLabel('exercise', activity)}</dd></div>
                </dl>
                <footer><TonAddressLink address={item.address} label={t('tonswap.viewContract')}/></footer>
              </article>
            );
          })}
        </div>}
        {history.release && series.length === 0 && <p className="tonswap-note">{t('tonswap.options.noSeries')}</p>}
        {templates.length > 0 && <p className="tonswap-note tonswap-options-template">
          {t('tonswap.options.templates')} · {templates.map(seriesTitle).join(' · ')}
        </p>}
        <article className="card tonswap-protocol-feed">
          <header>
            <div><h3>{t('tonswap.activity.title')}</h3><p>{t('tonswap.options.activityNote')}</p></div>
            <div className="tonswap-options-filters">
              {series.length > 1 && <select aria-label={t('tonswap.options.seriesFilter')} value={selected}
                onChange={event => setSelection(event.target.value)}>
                <option value="all">{t('tonswap.options.allSeries')}</option>
                {series.map(item => <option key={item.seriesId} value={item.seriesId}>{seriesLabel(item.seriesId)}</option>)}
              </select>}
              <div className="segmented" role="group" aria-label={t('tonswap.options.filters')}>
                {['all', 'buy', 'exercise'].map(action => <button key={action} type="button"
                  className={filter === action ? 'active' : ''} aria-pressed={filter === action}
                  onClick={() => setFilter(action)}>{t(action === 'all' ? 'tonswap.protocol.all' : 'tonswap.options.' + action + 'Filter')}</button>)}
              </div>
            </div>
          </header>
          <ProtocolActivity history={history} rows={rows} empty={'tonswap.options.empty.' + filter}>
            {row => <>
              <div><strong>{t('tonswap.options.' + row.kind)}</strong><TonEventDate seconds={row.utime}/></div>
              <div><strong>{seriesLabel(row.seriesId)}</strong>
                {row.position !== '0' && <small>{t('tonswap.options.positionId').replace('{id}', row.position)}</small>}</div>
              <div><strong>{formatTokenAmount(row.amount, 9, locale)} T3</strong>
                {row.kind !== 'fund' && <small>{t(row.kind === 'buy' ? 'tonswap.options.premium' : 'tonswap.options.payout')}</small>}</div>
              <div><small>{t('tonswap.t3.wallet')}</small><TonAddressLink address={row.owner}/></div>
              <TonTxLink hash={row.hash}/>
            </>}
          </ProtocolActivity>
        </article>
      </section>
    );
  }

  function SpotOrdersView() {
    const { t, locale } = useLang();
    const history = useProtocolHistory('orders');
    const [filter, setFilter] = useState('all');
    const now = Date.now() / 1000;
    const orders = history.rows.map(row => {
      const status = row.state === 3 ? [null, 'filled', 'cancelled', 'expired'][row.closeReason] :
        row.state === 2 ? 'executing' : row.expiresAt <= now ? 'awaitingClose' : 'open';
      return { ...row, status, isOpen: status === 'open' || status === 'executing' };
    });
    const rows = orders.filter(row => filter === 'all' || (filter === 'open' ? row.isOpen : row.executed > 0));
    const tokens = new Map((history.release?.tokens || []).map(token => [canonicalTonAddress(token.root), token]));
    const markets = new Map((history.release?.spotMarkets || []).map(market => [market.pool, market]));
    const complete = !history.loading && !history.error && !history.incomplete && !history.next;
    const counts = { all: orders.length, open: orders.filter(row => row.isOpen).length, executed: orders.filter(row => row.executed > 0).length };
    const statusColors = { open: 'info', executing: 'warn', filled: 'ok', awaitingClose: 'warn' };
    return (
      <section aria-labelledby="tonswap-orders-title" className="tonswap-orders">
        <ProtocolToolbar kind="orders" history={history} contractLabel="tonswap.orders.factory"/>
        <article className="card tonswap-protocol-feed">
          <header>
            <div><h3>{t('tonswap.orders.list')}</h3><p>{t('tonswap.orders.progressNote')}</p></div>
            <div className="segmented" role="group" aria-label={t('tonswap.orders.filters')}>
              {['all', 'open', 'executed'].map(action => <button key={action} type="button"
                className={filter === action ? 'active' : ''} aria-pressed={filter === action}
                onClick={() => setFilter(action)}>
                {t('tonswap.orders.filter.' + action)}{complete && <span className="tonswap-order-count"> {fmtNumber(counts[action], locale)}</span>}
              </button>)}
            </div>
          </header>
          <ProtocolActivity history={history} rows={rows} empty={'tonswap.orders.empty.' + filter}>
            {row => {
              const token = tokens.get(row.inputMaster);
              const market = markets.get(row.pool);
              const output = token?.symbol === 'T3' ? market?.symbol : token?.symbol === market?.symbol ? 'T3' : null;
              const amount = token && validDecimals(token.decimals) ?
                formatTokenAmount(row.amount, token.decimals, locale) + ' ' + token.symbol : null;
              const sold = amount ? formatTokenAmount(row.sold, token.decimals, locale) + ' ' + token.symbol : null;
              return <>
                <div><strong>{t(row.slices === 1 ? 'tonswap.orders.limit' : 'tonswap.orders.twap')}</strong>
                  <div><span className={'tag ' + (statusColors[row.status] || '')}>{t('tonswap.orders.status.' + row.status)}</span></div>
                  {row.cancelRequested && row.isOpen && <small>{t('tonswap.orders.cancelRequested')}</small>}
                  {row.status === 'open' && !row.inputWallet && <small>{t('tonswap.orders.waitingWallet')}</small>}
                </div>
                <div>
                  {token && output ? <strong>{token.symbol} → {output}</strong> : <TonAddressLink address={row.pool} label={t('tonswap.orders.pool')}/>}
                  {amount && <small>{t('tonswap.orders.input')} <bdi>{amount}</bdi></small>}
                </div>
                <div>
                  <small>{t('tonswap.orders.progress')}</small>
                  <strong>{fmtNumber(row.executed, locale)} / {fmtNumber(row.slices, locale)}</strong>
                  {row.executed > 0 && sold && <small>{t('tonswap.orders.sold')} <bdi>{sold}</bdi></small>}
                  {row.lastExecutedAt > 0 && <small>{t('tonswap.orders.lastSlice')} <TonEventDate seconds={row.lastExecutedAt}/></small>}
                </div>
                <div><small>{t('tonswap.t3.wallet')}</small><TonAddressLink address={row.owner}/>
                  <small>{t('tonswap.orders.expiry')} <TonEventDate seconds={row.expiresAt}/></small>
                </div>
                <div><TonAddressLink address={row.order} label={t('tonswap.viewContract')}/>
                  <TonTxLink hash={row.hash} label="tonswap.orders.request"/></div>
              </>;
            }}
          </ProtocolActivity>
        </article>
      </section>
    );
  }

  function TonswapSwaps({ visible }) {
    const t = useT();
    const [orders, setOrders] = useState(() => new URLSearchParams(window.location.search).get('tonswapSwaps') === 'orders');
    function selectOrders(show) {
      const url = new URL(window.location.href);
      if (show) url.searchParams.set('tonswapSwaps', 'orders');
      else url.searchParams.delete('tonswapSwaps');
      window.history.replaceState(window.history.state, '', url);
      setOrders(show);
    }
    return (
      <div hidden={!visible}>
        <div className="segmented tonswap-swap-views" role="group" aria-label={t('tonswap.swapViews')}>
          <button type="button" className={!orders ? 'active' : ''} aria-pressed={!orders} onClick={() => selectOrders(false)}>
            {t('tonswap.walletSwaps')}
          </button>
          <button type="button" className={orders ? 'active' : ''} aria-pressed={orders} onClick={() => selectOrders(true)}>
            {t('tonswap.orders.title')}
          </button>
        </div>
        <div hidden={orders}><TonswapWallet/></div>
        {visible && orders && <SpotOrdersView/>}
      </div>
    );
  }

  function TonswapProtocolStatus() {
    const { t, locale } = useLang();
    const [refresh, setRefresh] = useState(0);
    const [data, setData] = useState({ status: 'checking' });

    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
      async function load() {
        try {
          const [manifest, page] = await Promise.all([
            readJson(MANIFEST, signal),
            readResponse(TONSWAP_SITE, signal).then(response => response.text()).catch(() => null),
          ]);
          releaseTokens(manifest);
          if (typeof manifest.releaseId !== 'string' || !/^[a-z0-9-]{1,100}$/.test(manifest.releaseId) ||
              typeof manifest.manifestHash !== 'string' || !/^[a-f0-9]{64}$/.test(manifest.manifestHash)) {
            throw new Error('Unexpected Tonswap release identity');
          }
          const verified = manifest.releaseId === SCCP_DISABLED_RELEASE.id &&
            manifest.manifestHash === SCCP_DISABLED_RELEASE.hash &&
            page?.includes('src="./assets/' + SCCP_DISABLED_RELEASE.webEntry + '"');
          if (!signal.aborted) setData({
            status: verified ? 'disabled' : 'unverified', release: manifest.releaseId, checkedAt: new Date(),
          });
        } catch {
          if (!signal.aborted) setData({ status: 'error' });
        }
      }
      load();
      return () => controller.abort();
    }, [refresh]);

    const checking = data.status === 'checking';
    const ton = t('tonswap.testnet');
    const directions = [[ton, 'SORA'], ['SORA', ton]];
    const badge = {
      disabled: ['warn', 'tonswap.sccp.noRoute'],
      unverified: ['', 'tonswap.sccp.unverified'],
      error: ['', 'tonswap.unavailable'],
    }[data.status];
    const checked = data.checkedAt && new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(data.checkedAt);
    return (
      <section aria-labelledby="tonswap-status-title">
        <header className="tonswap-toolbar">
          <div>
            <h2 id="tonswap-status-title">{t('tonswap.sccp.statusTitle')}</h2>
            <p>{t('tonswap.sccp.statusDescription')}</p>
          </div>
          <div className="tonswap-toolbar-actions">
            {checked && <small>{t('tonswap.updated').replace('{time}', checked)}</small>}
            <button className="btn" type="button" disabled={checking} onClick={() => {
              setData({ status: 'checking' });
              setRefresh(previous => previous + 1);
            }}>{t('tonswap.refresh')}</button>
          </div>
        </header>
        <article className="card tonswap-protocol-feed tonswap-sccp" aria-labelledby="tonswap-sccp-title" aria-busy={checking}>
          <header>
            <div>
              <h3 id="tonswap-sccp-title">{t('tonswap.sccp.title')}</h3>
              <p role={data.status === 'error' ? 'alert' : 'status'}>{t(
                'tonswap.sccp.' + (data.status === 'unverified' ? 'unverifiedDetail' : data.status)
              )}</p>
            </div>
            {data.status === 'disabled' && <span className="tag warn">{t('tonswap.sccp.noActive')}</span>}
          </header>
          {badge && <ul className="tonswap-sccp-routes" aria-label={t('tonswap.sccp.directions')}>
            {directions.map(([from, to]) => <li key={from}>
              <strong>{t('tonswap.sccp.direction').replace('{from}', from).replace('{to}', to)}</strong>
              <span className={'tag ' + badge[0]}>{t(badge[1])}</span>
            </li>)}
          </ul>}
          <footer>
            {data.release && <small>{t('tonswap.sccp.release')} <bdi>{data.release}</bdi></small>}
            <div className="tonswap-sccp-sources">
              <a href={TONSWAP_SITE + '#/sccp'} target="_blank" rel="noopener noreferrer">
                {t('tonswap.sccp.official')} <span aria-hidden="true">↗</span>
              </a>
              <a href={MANIFEST} target="_blank" rel="noopener noreferrer">
                {t('tonswap.sccp.manifest')} <span aria-hidden="true">↗</span>
              </a>
            </div>
          </footer>
        </article>
      </section>
    );
  }

  function TonswapSection() {
    const t = useT();
    const [view, setView] = useState(() => {
      const requested = new URLSearchParams(window.location.search).get('tonswapView');
      return ['swaps', 'wallets', 'perps', 'options', 'launchpad', 't3', 'status'].includes(requested) ? requested : 'pools';
    });

    function selectView(next) {
      const url = new URL(window.location.href);
      if (next === 'pools') url.searchParams.delete('tonswapView');
      else url.searchParams.set('tonswapView', next);
      window.history.replaceState(window.history.state, '', url);
      setView(next);
    }
    return (
      <div className="tonswap-page">
        <div className="tonswap-heading">
          <PageHeader title="Tonswap" sub={t('tonswap.subtitle')}>
            <span className="tag info tonswap-network"><span aria-hidden="true"/> {t('tonswap.testnet')}</span>
            <span className="tonswap-test-assets">{t('tonswap.testAssets')}</span>
          </PageHeader>
        </div>
        <div className="segmented tonswap-views" role="group" aria-label={t('tonswap.views')}>
          <button type="button" className={view === 'pools' ? 'active' : ''} aria-pressed={view === 'pools'}
            onClick={() => selectView('pools')}>{t('nav.pools')}</button>
          <button type="button" className={view === 'swaps' ? 'active' : ''} aria-pressed={view === 'swaps'}
            onClick={() => selectView('swaps')}>{t('tonswap.swapsTitle')}</button>
          <button type="button" className={view === 't3' ? 'active' : ''} aria-pressed={view === 't3'}
            onClick={() => selectView('t3')}>T3</button>
          <button type="button" className={view === 'perps' ? 'active' : ''} aria-pressed={view === 'perps'}
            onClick={() => selectView('perps')}>{t('tonswap.perps.title')}</button>
          <button type="button" className={view === 'options' ? 'active' : ''} aria-pressed={view === 'options'}
            onClick={() => selectView('options')}>{t('tonswap.options.title')}</button>
          <button type="button" className={view === 'launchpad' ? 'active' : ''} aria-pressed={view === 'launchpad'}
            onClick={() => selectView('launchpad')}>{t('tonswap.launchpad.title')}</button>
          <button type="button" className={view === 'wallets' ? 'active' : ''} aria-pressed={view === 'wallets'}
            onClick={() => selectView('wallets')}>{t('tonswap.portfolio.title')}</button>
          <button type="button" className={view === 'status' ? 'active' : ''} aria-pressed={view === 'status'}
            onClick={() => selectView('status')}>{t('tonswap.sccp.statusTitle')}</button>
        </div>
        <div hidden={view !== 'pools'}><TonswapPools/></div>
        <TonswapSwaps visible={view === 'swaps'}/>
        <div hidden={view !== 'wallets'}><TonswapPortfolio/></div>
        {view === 't3' && <T3View/>}
        {view === 'perps' && <PerpsView/>}
        {view === 'options' && <OptionsView/>}
        {view === 'launchpad' && <LaunchpadView/>}
        {view === 'status' && <TonswapProtocolStatus/>}
      </div>
    );
  }

  window.TonswapSection = TonswapSection;
})();
