/* global TonswapBoc */
(() => {
  const { readCells, reader } = TonswapBoc;
  const PERPS_EVENT = 0x50584556;
  const SALE_REQUEST = 0x53464e57;
  const SALE_EVENT = 0x53464556;
  const SALE_INIT = 0x53494e49;
  const OPTION_EVENT = 0x4f564556;
  const CREATE_ORDER = 0x4f435254;
  const INIT_ORDER = 0x4f494e49;
  const T3_RECEIPT = 0x5433484c;
  const T3_MINT = 0x54334d4e;
  const T3_PAIR = 0x5433504d;
  const T3_REDEEM = 0x54335244;
  const perpsActions = {
    1: 'open', 2: 'increase', 3: 'addMargin', 4: 'removeMargin',
    5: 'close', 6: 'liquidate', 7: 'funding',
  };

  function readBody(body, opcode, referenceCount) {
    const { cells, root } = readCells(body);
    const value = reader(cells[root]);
    const refs = cells[root].references;
    if (value.uint(32) !== BigInt(opcode) || refs.length !== referenceCount) {
      throw new Error('Unexpected Tonswap event body');
    }
    return { value, refs, cells };
  }

  // PXEV emitted by the current PerpsEngine. Size is the position after execution.
  function perpsEvent(body) {
    const { value, refs, cells } = readBody(body, PERPS_EVENT, 1);
    const kind = Number(value.uint(8));
    const reason = value.uint(16);
    const owner = value.address();
    const marketId = Number(value.uint(32));
    const queryId = value.uint(64).toString();
    value.end();
    if (kind < 1 || kind > 11 || reason > 10n) throw new Error('Unexpected Perps event kind');
    if (!perpsActions[kind] || reason !== 0n) return null;
    if (cells[refs[0]].references.length) throw new Error('Unexpected Perps event detail');
    const detail = reader(cells[refs[0]]);
    const size = detail.int(128);
    const fill = detail.coins();
    const margin = detail.int(128);
    const fee = detail.coins();
    detail.coins(); // Planned LP payout.
    detail.coins(); // Unpaid loss.
    detail.end();
    return { kind: perpsActions[kind], owner, marketId, queryId, size, fill, margin, fee };
  }

  function saleEvent(body, tx, address) {
    const { value } = readBody(body, SALE_EVENT, 0);
    const owner = value.address();
    const nonce = value.uint(64);
    const mode = Number(value.uint(8));
    const sale = value.address();
    value.end();
    const incoming = tx.inMessage;
    if (incoming?.op !== SALE_REQUEST || address(incoming.source) !== owner ||
        !tx.outMessages.some(message => message.op === SALE_INIT && address(message.destination) === sale)) {
      throw new Error('Missing matching sale request or init');
    }
    const { value: request, refs, cells } = readBody(incoming.body, SALE_REQUEST, 1);
    request.uint(64); // Query ID is independent of the sale nonce.
    if (request.uint(64) !== nonce || Number(request.uint(8)) !== mode || mode > 2) {
      throw new Error('Mismatched sale nonce or mode');
    }
    request.end();
    const termsCell = cells[refs[0]];
    const terms = reader(termsCell);
    const token = terms.address();
    terms.address(); // LP recipient.
    const decimals = Number(terms.uint(8));
    const starts = Number(terms.uint(32));
    const ends = Number(terms.uint(32));
    terms.int(32); // Seed bin.
    const vesting = Number(terms.uint(1));
    terms.end();
    if (termsCell.references.length !== 2 + vesting || decimals > 30 || starts >= ends) {
      throw new Error('Invalid sale terms');
    }
    const caps = reader(cells[termsCell.references[0]]);
    caps.coins(); // Soft cap.
    const hardCap = caps.coins();
    caps.coins(); // Minimum contribution.
    caps.coins(); // Maximum contribution.
    const supply = caps.coins();
    caps.end();
    return { kind: ['fixed', 'bonding', 'auction'][mode], owner, sale, token, decimals, starts, ends, hardCap, supply };
  }

  function contractEvents(transactions, contract, contractAddress, address) {
    const opcode = contract === 'perps' ? PERPS_EVENT : SALE_EVENT;
    const rows = [];
    let incomplete = false;
    for (const tx of transactions) {
      if (tx?.status !== 'success') continue;
      if (!Array.isArray(tx.outMessages)) {
        incomplete = true;
        continue;
      }
      tx.outMessages.forEach((message, index) => {
        if (message?.op !== opcode) return;
        try {
          if (message.destination != null || address(message.source) !== contractAddress) {
            throw new Error('Unexpected Tonswap event source');
          }
          const event = contract === 'perps' ? perpsEvent(message.body) : saleEvent(message.body, tx, address);
          if (event) rows.push({ ...event, index, hash: tx.hash, utime: tx.utime });
        } catch {
          incomplete = true;
        }
      });
    }
    return { rows, incomplete };
  }

  function perpsMarket({ exit_code, stack }, expectedPool) {
    if (exit_code !== 0 || stack?.length !== 1 || stack[0]?.[0] !== 'cell') {
      throw new Error('Unexpected Perps market response');
    }
    const { cells, root } = readCells(stack[0][1]?.bytes);
    const value = reader(cells[root]);
    const [paramsIndex, totalsIndex] = cells[root].references;
    if (value.address() !== expectedPool || cells[root].references.length !== 3) throw new Error('Unexpected Perps pool');
    value.end();
    const params = reader(cells[paramsIndex]);
    params.coins(); // Depth unit.
    const cap = params.coins();
    params.int(128); // Impact alpha.
    params.int(128); // Impact beta.
    const leverage = Number(params.uint(32)) / 10_000;
    const totals = reader(cells[totalsIndex]);
    const long = totals.coins();
    const short = totals.coins();
    totals.coins(); // Pending open interest.
    totals.end();
    return { cap, leverage, long, short };
  }

  function getterCell({ exit_code, stack }) {
    if (exit_code !== 0 || stack?.length !== 1 || stack[0]?.[0] !== 'cell') {
      throw new Error('Unexpected TON getter response');
    }
    return readCells(stack[0][1]?.bytes);
  }

  function optionSeriesTerms(result) {
    const { cells, root } = getterCell(result);
    const terms = cells[cells[root].references[0]];
    if (!terms || terms.references.length !== 1) throw new Error('Missing option series terms');
    const value = reader(terms);
    value.address(); // Writer.
    const address = value.address();
    const kind = Number(value.uint(16));
    const expiry = Number(value.uint(32));
    value.coins(); // Maximum notional.
    value.uint(64); // Premium and collateral rates.
    value.coins(); // Required backing.
    value.end();
    return { address, kind: { 1: 'shout', 2: 'outperformance' }[kind], expiry };
  }

  function optionCapacity({ exit_code, stack }) {
    if (exit_code !== 0 || stack?.length !== 3 || stack.some(item => item?.[0] !== 'num' || !/^\d+$/.test(item[1]))) {
      throw new Error('Unexpected option capacity');
    }
    const [count, , limit] = stack.map(item => Number(item[1]));
    if (!Number.isSafeInteger(count) || !Number.isSafeInteger(limit) || limit <= 0 || count > limit) {
      throw new Error('Invalid option capacity');
    }
    return { count, limit };
  }

  function t3Backing(result) {
    const { cells, root } = getterCell(result);
    const value = reader(cells[root]);
    const amounts = [value.coins(), value.coins(), value.coins()];
    const supply = value.coins();
    value.uint(23); // Fee, admitted and frozen masks, and withdrawals-only flag.
    value.end();
    if (cells[root].references.length !== 2 || (supply > 0n && supply < 1_000_000_000n)) {
      throw new Error('Unexpected T3 economy');
    }
    // get_jetton_data excludes the 1 T3 reserved by the protocol at initial issuance.
    return { amounts, supply: supply > 0n ? supply - 1_000_000_000n : 0n };
  }

  function t3ReserveMasters(result) {
    const { cells, root } = getterCell(result);
    const ledger = reader(cells[root]);
    const hasSlots = Number(ledger.uint(1));
    const hasOwed = Number(ledger.uint(1));
    ledger.end();
    if (!hasSlots || cells[root].references.length !== hasSlots + hasOwed) throw new Error('Unexpected T3 ledger');
    const masters = [];
    // Decode the first HashmapE (uint8 slot -> reserve), leaving the owed dictionary unread.
    function visit(index, remaining, prefix) {
      const { references } = cells[index];
      const entry = reader(cells[index]);
      const lengthBits = 32 - Math.clz32(remaining);
      let length;
      let label;
      if (entry.uint(1) === 0n) {
        length = 0;
        while (entry.uint(1) === 1n) {
          if (++length > remaining) throw new Error('Invalid T3 slot prefix');
        }
        label = Number(entry.uint(length));
      } else {
        const same = entry.uint(1) === 1n;
        const bit = same ? entry.uint(1) : 0n;
        length = Number(entry.uint(lengthBits));
        if (length > remaining) throw new Error('Invalid T3 slot prefix');
        label = same ? Number(bit) * ((1 << length) - 1) : Number(entry.uint(length));
      }
      prefix = (prefix << length) | label;
      remaining -= length;
      if (remaining === 0) {
        if (prefix > 2 || masters[prefix]) throw new Error('Invalid T3 reserve slot');
        masters[prefix] = entry.address();
        return;
      }
      entry.end();
      if (references.length !== 2) throw new Error('Invalid T3 slot branch');
      visit(references[0], remaining - 1, prefix << 1);
      visit(references[1], remaining - 1, (prefix << 1) | 1);
    }
    visit(cells[root].references[0], 8, 0);
    return masters;
  }

  function hexBody(hex, maxLength = 12000) {
    if (typeof hex !== 'string' || hex.length > maxLength || !/^(?:[a-f0-9]{2})+$/i.test(hex)) {
      throw new Error('Invalid TON BoC');
    }
    return btoa(hex.match(/../g).map(byte => String.fromCharCode(parseInt(byte, 16))).join(''));
  }

  function cellReader(cells, index) {
    const cell = cells[index];
    const value = reader(cell);
    let ref = 0;
    return {
      ...value,
      ref(optional = false) {
        if (optional && value.uint(1) === 0n) return null;
        if (ref >= cell.references.length) throw new Error('Truncated TON cell references');
        return cell.references[ref++];
      },
      end() {
        value.end();
        if (ref !== cell.references.length) throw new Error('Unexpected TON cell references');
      },
    };
  }

  const SWAP_EVENT = 0x53574533; // SWE3
  const SWAP_REQUEST = 0x53575033; // SWP3
  const JETTON_NOTIFICATION = 0x7362d09c;
  const LIQUIDITY_EVENT = 0x4c514533;
  const FAUCET_CLAIM = 0x46434c4d;
  const JETTON_TRANSFER = 0x0f8a7ea5;
  const JETTON_DELIVERY = 0x178d4519;
  const T3_DELIVERY = 0x5433444c;
  const tonOp = message => Number(message?.op_code);
  const succeeded = tx => tx?.success === true && tx.aborted === false;

  function directSwap(message, event) {
    if (tonOp(message) !== JETTON_NOTIFICATION || message.bounced) return false;
    const { cells, root } = readCells(hexBody(message.raw_body));
    let payload = cellReader(cells, root);
    if (payload.uint(32) !== BigInt(JETTON_NOTIFICATION) || payload.uint(64) !== event.queryId ||
        payload.coins() !== event.amountIn || payload.address() !== event.origin) return false;
    if (payload.uint(1) === 1n) {
      const index = payload.ref();
      payload.end();
      payload = cellReader(cells, index);
    }
    if (payload.uint(32) !== BigInt(SWAP_REQUEST)) return false;
    payload.coins(); // Minimum output, not the amount executed.
    payload.uint(32); // Deadline.
    payload.address(); // Recipient.
    payload.address(); // Refund address.
    const notifyOrigin = payload.uint(1);
    payload.coins();
    payload.coins();
    const forward = payload.ref(true);
    payload.end();
    // Coordinated routes and order fills request a callback or carry a continuation.
    return notifyOrigin === 0n && forward === null;
  }

  function walletPoolEvents(transactions, pool, wallet, liquidityEvents) {
    const rows = [];
    const liquidityTransactions = [];
    let incomplete = false;
    for (const tx of transactions) {
      if (!succeeded(tx)) continue;
      if (!Array.isArray(tx.out_msgs)) {
        incomplete = true;
        continue;
      }
      const liquidity = [];
      for (const [index, message] of tx.out_msgs.entries()) {
        const op = tonOp(message);
        if (op !== SWAP_EVENT && op !== LIQUIDITY_EVENT) continue;
        if (message.msg_type !== 'ext_out_msg' || message.source?.address !== pool || message.destination != null) continue;
        try {
          const body = hexBody(message.raw_body);
          if (op === LIQUIDITY_EVENT) {
            liquidity.push({ op, body });
            continue;
          }
          const { value } = readBody(body, SWAP_EVENT, 0);
          const queryId = value.uint(64);
          const origin = value.address();
          const side = Number(value.uint(8));
          const status = Number(value.uint(8));
          const reason = Number(value.uint(8));
          const amountIn = value.coins();
          const amountOut = value.coins();
          value.coins(); // Gross fee.
          value.coins(); // Protocol fee.
          value.int(32); // Active bin.
          value.uint(8); // Bins loaded.
          value.end();
          if (side > 1 || status > 3 || (status === 0 && (reason !== 0 || amountIn <= 0n || amountOut <= 0n))) {
            throw new Error('Invalid pool swap receipt');
          }
          if (origin === wallet && status === 0 && directSwap(tx.in_msg, { queryId, origin, amountIn })) {
            rows.push({ kind: 'swap', hash: tx.hash, index, utime: tx.utime, side, amountIn, amountOut });
          }
        } catch {
          incomplete = true;
        }
      }
      if (liquidity.length) {
        let inMessage;
        if (tonOp(tx.in_msg) === JETTON_NOTIFICATION && !tx.in_msg.bounced) {
          try { inMessage = { op: JETTON_NOTIFICATION, body: hexBody(tx.in_msg.raw_body) }; }
          catch { incomplete = true; }
        }
        liquidityTransactions.push({ status: 'success', hash: tx.hash, utime: tx.utime, inMessage, outMessages: liquidity });
      }
    }
    const decoded = liquidityEvents(liquidityTransactions);
    for (const event of decoded.rows) {
      if (event.owner !== wallet && event.payer !== wallet) continue;
      rows.push({ ...event, index: event.key, kind: ['deposit', 'withdrawal', 'eviction'][event.kind - 1] });
    }
    return { rows, incomplete: incomplete || decoded.incomplete };
  }

  // The faucet uses a new wire query for each grant, independent of the claim query.
  function faucetGrant(tx, faucet, wallet) {
    if (!succeeded(tx) || tonOp(tx.in_msg) !== FAUCET_CLAIM ||
        tx.in_msg.source?.address !== wallet || tx.in_msg.bounced) return null;
    const { value } = readBody(hexBody(tx.in_msg.raw_body), FAUCET_CLAIM, 0);
    value.uint(64); // Claim query ID.
    const symbol = ['T3', 'USDT', 'TS'][Number(value.uint(8))];
    value.end();
    const dispatches = tx.out_msgs.filter(message => tonOp(message) === JETTON_TRANSFER &&
      message.source?.address === faucet && message.msg_type === 'int_msg' && !message.bounced);
    if (!symbol || dispatches.length !== 1) throw new Error('Missing faucet dispatch');
    const dispatch = dispatches[0];
    const { value: transfer } = readBody(hexBody(dispatch.raw_body), JETTON_TRANSFER, 0);
    const wire = transfer.uint(64);
    const amount = transfer.coins();
    if (amount <= 0n || transfer.address() !== wallet || transfer.address() !== wallet ||
        transfer.uint(1) !== 0n || transfer.coins() !== 0n || transfer.uint(1) !== 0n) {
      throw new Error('Unexpected faucet dispatch');
    }
    transfer.end();
    return { symbol, wire, amount, dispatch, claimHash: tx.hash };
  }

  function faucetCredit(trace, grant, faucet, wallet) {
    const nodes = [trace];
    let claim;
    for (let i = 0; i < nodes.length && i < 512; i++) {
      const node = nodes[i];
      if (node?.transaction?.hash === grant.claimHash) {
        claim = node;
        break;
      }
      if (Array.isArray(node?.children)) nodes.push(...node.children);
    }
    const debit = claim?.children?.find(node => node.transaction?.in_msg?.hash === grant.dispatch.hash);
    const from = grant.dispatch.destination?.address;
    if (!succeeded(claim?.transaction) || claim.transaction.account?.address !== faucet || !from ||
        debit?.transaction?.account?.address !== from || debit.transaction.in_msg.source?.address !== faucet ||
        debit.transaction.in_msg.bounced || tonOp(debit.transaction.in_msg) !== JETTON_TRANSFER) {
      throw new Error('Faucet transfer not found in trace');
    }
    if (!succeeded(debit.transaction)) return null;
    const isT3 = grant.symbol === 'T3';
    const op = isT3 ? T3_DELIVERY : JETTON_DELIVERY;
    const credits = debit.children?.map(node => node.transaction).filter(tx =>
      tonOp(tx?.in_msg) === op && tx.in_msg.source?.address === from &&
      tx.in_msg.destination?.address === tx.account?.address && !tx.in_msg.bounced);
    if (credits?.length !== 1) throw new Error('Faucet credit not found in trace');
    const credit = credits[0];
    if (!succeeded(credit)) {
      if (isT3) throw new Error('T3 delivery may be retried');
      return null;
    }
    if (isT3) {
      const { value, refs, cells } = readBody(hexBody(credit.in_msg.raw_body), op, 1);
      value.end();
      const attempt = cellReader(cells, refs[0]);
      attempt.uint(64); // Source wallet operation ID.
      if (attempt.uint(64) !== BigInt(debit.transaction.lt) || attempt.uint(64) !== 0n) {
        throw new Error('Unexpected T3 delivery attempt');
      }
      const terms = cellReader(cells, attempt.ref());
      attempt.end();
      if (terms.address() !== faucet || terms.address() !== wallet ||
          terms.uint(1) !== 0n || terms.coins() !== grant.amount) throw new Error('Unexpected T3 delivery terms');
      const details = cellReader(cells, terms.ref());
      terms.end();
      if (details.uint(64) !== grant.wire || details.maybeAddress() !== wallet ||
          details.coins() !== 0n) throw new Error('Unexpected T3 delivery details');
      const forward = cells[details.ref()];
      if (forward.bits !== '0' || forward.references.length || details.ref(true) !== null) {
        throw new Error('Unexpected T3 delivery details');
      }
      details.end();
    } else {
      const { value } = readBody(hexBody(credit.in_msg.raw_body), op, 0);
      if (value.uint(64) !== grant.wire || value.coins() !== grant.amount || value.address() !== faucet ||
          value.maybeAddress() !== wallet || value.coins() !== 0n || value.uint(1) !== 0n) {
        throw new Error('Unexpected jetton delivery');
      }
      value.end();
    }
    return { kind: 'faucet', index: 0, hash: credit.hash, utime: credit.utime, amount: grant.amount, symbol: grant.symbol };
  }

  function t3MintRecord(cells, record, lt) {
    const phase = Number(record.uint(3));
    record.uint(66); // Linked slot and key.
    const creditIndex = record.ref();
    const mintIndex = record.ref(true);
    record.ref(true); // Payout.
    record.ref(true); // Staged credit.
    record.end();
    if (phase > 4) throw new Error('Invalid T3 receipt phase');
    if (phase !== 2 || mintIndex === null) return null;
    const mint = cellReader(cells, mintIndex);
    const amount = mint.coins();
    const recipient = mint.address();
    const attempt = mint.uint(64);
    const sentLt = mint.uint(64);
    const failed = mint.uint(1);
    mint.end();
    // Recovery and retry receipts keep the same operation key.
    if (attempt !== 0n || sentLt !== lt || failed !== 0n) return null;
    const credit = cellReader(cells, creditIndex);
    credit.uint(1); // Certified deposit.
    const owner = credit.address();
    credit.uint(64); // Query ID.
    credit.coins(); // Deposit.
    const intent = cellReader(cells, credit.ref());
    credit.ref(true); // Credit evidence.
    credit.end();
    const op = Number(intent.uint(32));
    if (op !== T3_MINT && op !== T3_PAIR) return null;
    intent.coins(); // Minimum output.
    if (intent.address() !== recipient || amount <= 0n) throw new Error('Unexpected T3 mint amount or recipient');
    intent.end();
    return { kind: 'mint', owner, recipient, amount, paired: op === T3_PAIR };
  }

  function t3RedeemRecord(cells, record, lt) {
    const attemptIndex = record.ref();
    const payoutIndex = record.ref(true);
    record.end();
    if (payoutIndex === null) return null; // A pure burn does not withdraw backing.
    const attempt = cellReader(cells, attemptIndex);
    attempt.uint(192);
    const terms = cellReader(cells, attempt.ref());
    attempt.end();
    const owner = terms.address();
    if (terms.address() !== owner || terms.uint(1) !== 1n) throw new Error('Unexpected T3 burn terms');
    const amount = terms.coins();
    const details = cells[terms.ref()];
    terms.end();
    if (details.references.length !== 2) throw new Error('Missing T3 redemption intent');
    const intent = cellReader(cells, details.references[1]);
    if (intent.uint(32) !== BigInt(T3_REDEEM)) throw new Error('Unexpected T3 burn intent');
    intent.uint(4); // Single-reserve flag, reserve index and strict flag.
    intent.coins(); // Minimum output.
    const payout = cellReader(cells, payoutIndex);
    if (payout.address() !== owner || amount <= 0n) throw new Error('Invalid T3 redemption');
    const recipient = payout.address();
    if (intent.address() !== recipient) throw new Error('Unexpected T3 redemption recipient');
    intent.end();
    payout.uint(64);
    const legs = cellReader(cells, payout.ref());
    payout.end();
    const reserves = [0, 1, 2].map(() => {
      const leg = cellReader(cells, legs.ref());
      const reserve = leg.coins();
      const attemptId = leg.uint(64);
      const sentLt = leg.uint(64);
      const failed = leg.uint(1);
      if (leg.address() !== recipient || (reserve > 0n && (sentLt !== lt || attemptId !== 0n || failed !== 0n))) {
        throw new Error('Unexpected T3 withdrawal leg');
      }
      leg.end();
      return reserve;
    });
    legs.end();
    if (reserves.every(reserve => reserve === 0n)) throw new Error('Empty T3 redemption');
    // The current hub attaches a payout to burns only for a valid T3_REDEEM intent.
    return { kind: 'redeem', owner, recipient, amount, reserves };
  }

  function t3Event(body, lt) {
    const { value, refs, cells } = readBody(hexBody(body), T3_RECEIPT, 1);
    const slot = Number(value.uint(2));
    value.end();
    const receipt = cellReader(cells, refs[0]);
    const key = receipt.uint(64);
    const record = cellReader(cells, receipt.ref());
    receipt.end();
    if (slot === 3) return key === lt ? t3RedeemRecord(cells, record, lt) : null;
    return t3MintRecord(cells, record, lt);
  }

  function tonProtocolEvents(transactions, address, opcode, decode) {
    const rows = [];
    let incomplete = false;
    for (const tx of transactions) {
      if (tx.success !== true || tx.aborted !== false) continue;
      if (!Array.isArray(tx.out_msgs)) {
        incomplete = true;
        continue;
      }
      tx.out_msgs.forEach((message, index) => {
        if (Number(message?.op_code) !== opcode) return;
        try {
          if (message.msg_type !== 'ext_out_msg' || message.destination != null ||
              message.source?.address !== address) {
            throw new Error('Unexpected TON event source');
          }
          const event = decode(message.raw_body, tx);
          if (event) rows.push({ ...event, index, hash: tx.hash, utime: tx.utime });
        } catch {
          incomplete = true;
        }
      });
    }
    return { rows, incomplete };
  }

  function t3Events(transactions, address) {
    return tonProtocolEvents(transactions, address, T3_RECEIPT, (body, tx) => {
      if (!Number.isSafeInteger(tx.lt) || tx.lt <= 0) throw new Error('Invalid T3 transaction LT');
      return t3Event(body, BigInt(tx.lt));
    });
  }

  function optionEvents(transactions, vault, factory) {
    return tonProtocolEvents(transactions, vault, OPTION_EVENT, (body, tx) => {
      const { value } = readBody(hexBody(body), OPTION_EVENT, 0);
      value.uint(64); // Query ID.
      const type = Number(value.uint(8));
      const seriesId = value.uint(64);
      const position = value.uint(64);
      const intent = value.uint(64);
      const amount = value.coins();
      const owner = value.address();
      value.end();
      if (type < 1 || type > 7 || seriesId === 0n) throw new Error('Invalid option event');
      const kind = { 1: 'fund', 2: 'buy', 4: 'exercise' }[type];
      if (!kind) return null;
      const incoming = tx.in_msg;
      const expectedOp = kind === 'exercise' ? 0x4f565354 : 0x4f56434d; // OVST or OVCM.
      if (incoming?.msg_type !== 'int_msg' || incoming.bounced || incoming.source?.address !== factory ||
          Number(incoming.op_code) !== expectedOp ||
          (kind === 'fund' ? position !== 0n : position === 0n || position !== intent)) {
        throw new Error('Unexpected option event transaction');
      }
      return { kind, seriesId: seriesId.toString(), position: position.toString(), amount, owner };
    });
  }

  async function cellHashes(cells) {
    const hashes = [];
    const depths = [];
    // TON representation: descriptors and data, all reference depths, then all reference hashes.
    for (let index = cells.length - 1; index >= 0; index--) {
      const { data, references } = cells[index];
      const representation = new Uint8Array(data.length + 34 * references.length);
      representation.set(Array.from(data, byte => byte.charCodeAt(0)));
      references.forEach((child, offset) => {
        const depth = depths[child];
        representation[data.length + offset * 2] = depth >> 8;
        representation[data.length + offset * 2 + 1] = depth & 255;
        representation.set(hashes[child], data.length + references.length * 2 + offset * 32);
      });
      depths[index] = references.length ? Math.max(...references.map(child => depths[child])) + 1 : 0;
      hashes[index] = new Uint8Array(await crypto.subtle.digest('SHA-256', representation));
    }
    return hashes;
  }

  const toHex = bytes => Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');

  async function stateInitHashes(hex) {
    const { cells, root } = readCells(hexBody(hex));
    const value = reader(cells[root]);
    if (value.uint(5) !== 6n || cells[root].references.length !== 2) return null;
    value.end();
    const hashes = await cellHashes(cells);
    return { root: toHex(hashes[root]), code: toHex(hashes[cells[root].references[0]]) };
  }

  async function spotOrderCodeHash(hex) {
    const { cells, root } = readCells(hexBody(hex, 24000), { maxLength: 16000, maxCells: 256 });
    return toHex((await cellHashes(cells))[root]);
  }

  function spotOrderRequests(transactions, factory, address) {
    const orders = new Map();
    let incomplete = false;
    for (const tx of transactions) {
      if (tx.success !== true || tx.aborted !== false || Number(tx.in_msg?.op_code) !== CREATE_ORDER) continue;
      try {
        const incoming = tx.in_msg;
        const owner = address(incoming.source?.address);
        if (!owner?.startsWith('0:') || incoming.msg_type !== 'int_msg' || incoming.bounced ||
            !Number.isSafeInteger(tx.lt) || tx.lt <= 0) throw new Error('Invalid order request');
        const { value } = readBody(hexBody(incoming.raw_body), CREATE_ORDER, 1);
        const query = value.uint(64);
        const nonce = value.uint(64);
        value.end();
        const inits = tx.out_msgs.filter(message => Number(message.op_code) === INIT_ORDER);
        if (inits.length !== 1) throw new Error('Missing order initialization');
        const message = inits[0];
        const order = address(message.destination?.address);
        if (!order?.startsWith('0:') || message.msg_type !== 'int_msg' || message.bounced ||
            message.source?.address !== factory || !message.init?.boc) throw new Error('Invalid order initialization');
        const init = readBody(hexBody(message.raw_body), INIT_ORDER, 1).value;
        if (init.uint(64) !== query) throw new Error('Unexpected order query');
        init.end();
        if (!orders.has(order)) orders.set(order, { order, owner, nonce, hash: tx.hash, index: 0, utime: tx.utime, lt: tx.lt });
      } catch {
        incomplete = true;
      }
    }
    return { orders: [...orders.values()], incomplete };
  }

  function spotOrderState(data, factory, order) {
    const { cells, root } = readCells(hexBody(data));
    const storage = cellReader(cells, root);
    if (storage.address() !== factory || storage.address() !== order.owner || storage.uint(64) !== order.nonce) {
      throw new Error('Unexpected order identity');
    }
    const body = cellReader(cells, storage.ref(true));
    storage.end();
    const state = Number(body.uint(2));
    const closeReason = Number(body.uint(2));
    const executed = Number(body.uint(8));
    const attempts = Number(body.uint(32));
    const lastExecutedAt = Number(body.uint(32));
    body.uint(32); // Last attempt.
    const cancelRequested = body.uint(1) === 1n;
    body.uint(5); // Refund observation, kind, and refusal.
    const peers = cellReader(cells, body.ref());
    const terms = cellReader(cells, body.ref());
    body.end();
    const inputWallet = peers.maybeAddress();
    peers.maybeAddress(); // Keeper.
    peers.maybeAddress(); // Previous keeper.
    peers.end();
    const inputMaster = terms.address();
    const pool = terms.address();
    terms.uint(1); // Sell side.
    terms.address(); // Recipient.
    const numbers = cellReader(cells, terms.ref());
    terms.end();
    const amount = numbers.coins();
    const minimum = numbers.coins();
    const slices = Number(numbers.uint(8));
    const interval = Number(numbers.uint(32));
    const startAt = Number(numbers.uint(32));
    const expiresAt = Number(numbers.uint(32));
    numbers.coins(); // Tip per slice.
    numbers.uint(64); // Forward gas and wallet gas.
    numbers.coins(); // Wallet GRAM budget.
    numbers.maybeAddress(); // Optional executor.
    numbers.ref(); // Wallet budget.
    numbers.end();
    if (state === 0 || (state === 3 ? closeReason === 0 : closeReason !== 0) || slices < 1 || slices > 50 ||
        executed > slices || executed > attempts || (executed === slices) !== (state === 3 && closeReason === 1) ||
        amount < BigInt(slices) || minimum <= 0n || (slices === 1 ? interval !== 0 : interval < 120 || interval > 86400) ||
        startAt + (slices - 1) * interval >= expiresAt) throw new Error('Invalid order execution state');
    const sold = executed === slices ? amount : BigInt(executed) * (amount / BigInt(slices));
    return { state, closeReason, executed, lastExecutedAt, cancelRequested, inputWallet, inputMaster, pool, amount, slices, expiresAt, sold };
  }

  window.TonswapProtocol = {
    contractEvents, perpsMarket, t3Backing, t3ReserveMasters, t3Events,
    optionSeriesTerms, optionCapacity, optionEvents, stateInitHashes,
    spotOrderCodeHash, spotOrderRequests, spotOrderState,
    walletPoolEvents, faucetGrant, faucetCredit,
  };
})();
