Skip to content
markpaper

src/signer/decode.ts

v0.1.0 · 6.4 KB

Download file
import { isSignatureString } from '../address/base58.js';
import { BID_SEQ_MIN, parseU64 } from '../numbers/ints.js';
export interface PhxCpi {
  priceInTicks: bigint;
  seq: bigint;
  basePosted: bigint;
  filledBaseLots: bigint;
  filledQuoteLots: bigint;
}
export interface PhxCancelOutcome {
  effective: boolean;
  notFound: Array<{
    priceInTicks: string;
    seq: string;
    reason: string;
  }>;
}
export interface PhxWriteAnswer {
  status: 'confirmed' | 'failed' | 'unknown';
  intentId: string | null;
  signature: string | null;
  slot: number | null;
  lastValidSlot: number | null;
  cpi: PhxCpi | null;
  cpiProblem: string | null;
  rejectReason: string | null;
  rejectMessage: string | null;
  cancel: PhxCancelOutcome | null;
  notSent: boolean;
  expired: boolean;
  activated: boolean;
  roClamped: {
    requestedLots: string;
    effectiveLots: string;
  } | null;
  explicitUnknown: boolean;
}
function parseSlot(v: unknown): number | null {
  if (typeof v === 'number') return Number.isSafeInteger(v) && v >= 0 ? v : null;
  const b = parseU64(v);
  return b !== null && b <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(b) : null;
}
const str = (v: unknown): string | null => (typeof v === 'string' && v.length > 0 ? v : null);
export function decodeCpi(
  raw: unknown,
  side: 'bid' | 'ask' | null,
): {
  cpi: PhxCpi | null;
  problem: string | null;
} {
  if (raw === null || raw === undefined) return { cpi: null, problem: 'Missing or non-object CPI response' };
  if (typeof raw !== 'object' || Array.isArray(raw))
    return { cpi: null, problem: 'Missing or non-object CPI response' };
  const c = raw as Record<string, unknown>;
  const priceInTicks = parseU64(c.priceInTicks);
  const seq = parseU64(c.seq);
  const basePosted = parseU64(c.basePosted);
  const filledBaseLots = parseU64(c.filledBaseLots);
  const filledQuoteLots = parseU64(c.filledQuoteLots);
  if (priceInTicks === null || seq === null || basePosted === null)
    return { cpi: null, problem: 'CPI priceInTicks, seq and basePosted require u64 strings' };
  if (filledBaseLots === null || filledQuoteLots === null)
    return { cpi: null, problem: 'CPI fill amounts require u64 strings' };
  if (basePosted > 0n && priceInTicks === 0n) return { cpi: null, problem: 'CPI posted order has a zero price' };
  if (basePosted > 0n && side !== null && seq >= BID_SEQ_MIN !== (side === 'bid')) {
    return { cpi: null, problem: `CPI sequence ${seq} disagrees with order side ${side}` };
  }
  if ((filledBaseLots === 0n) !== (filledQuoteLots === 0n))
    return { cpi: null, problem: 'CPI base and quote fills disagree' };
  return { cpi: { priceInTicks, seq, basePosted, filledBaseLots, filledQuoteLots }, problem: null };
}
function decodeCancel(raw: unknown): PhxCancelOutcome | null {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
  const c = raw as Record<string, unknown>;
  if (typeof c.effective !== 'boolean' || !Array.isArray(c.notFound)) return null;
  const notFound: PhxCancelOutcome['notFound'] = [];
  for (const x of c.notFound as unknown[]) {
    if (!x || typeof x !== 'object') return null;
    const r = x as Record<string, unknown>;
    if (parseU64(r.priceInTicks) === null || parseU64(r.seq) === null) return null;
    notFound.push({
      priceInTicks: String(r.priceInTicks),
      seq: String(r.seq),
      reason: typeof r.reason === 'string' ? r.reason : '',
    });
  }
  return { effective: c.effective, notFound };
}
export function decodeWriteAnswer(
  raw: unknown,
  opts: {
    intentId?: string;
    expectedSignature?: string;
    side?: 'bid' | 'ask' | null;
  } = {},
): PhxWriteAnswer {
  const empty: PhxWriteAnswer = {
    status: 'unknown',
    intentId: null,
    signature: null,
    slot: null,
    lastValidSlot: null,
    cpi: null,
    cpiProblem: null,
    rejectReason: null,
    rejectMessage: null,
    cancel: null,
    notSent: false,
    expired: false,
    activated: false,
    roClamped: null,
    explicitUnknown: false,
  };
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
    return { ...empty, rejectMessage: 'Signer response must be an object' };
  const b = raw as Record<string, unknown>;
  const signature = typeof b.signature === 'string' && isSignatureString(b.signature) ? b.signature : null;
  const intentId = typeof b.intentId === 'string' ? b.intentId : null;
  const out: PhxWriteAnswer = {
    ...empty,
    intentId,
    signature,
    slot: parseSlot(b.slot),
    lastValidSlot: parseSlot(b.lastValidSlot),
    rejectReason: str(b.rejectReason),
    rejectMessage: str(b.rejectMessage),
    notSent: b.notSent === true,
    expired: b.expired === true,
    activated: b.activated === true,
  };
  if (b.roClamped && typeof b.roClamped === 'object') {
    const r = b.roClamped as Record<string, unknown>;
    if (parseU64(r.requestedLots) !== null && parseU64(r.effectiveLots) !== null) {
      out.roClamped = { requestedLots: String(r.requestedLots), effectiveLots: String(r.effectiveLots) };
    }
  }
  if (opts.intentId !== undefined && intentId !== opts.intentId) {
    return {
      ...out,
      status: 'unknown',
      signature: null,
      rejectMessage: `Signer returned a different intentId (${intentId ?? 'missing'}, expected ${opts.intentId})`,
    };
  }
  if (opts.expectedSignature !== undefined && signature !== opts.expectedSignature) {
    return {
      ...out,
      status: 'unknown',
      signature: null,
      rejectMessage: 'Signer response signature differs from requested transaction',
    };
  }
  const status = b.status;
  if (status === 'failed') return { ...out, status: 'failed' };
  if (status === 'unknown') return { ...out, status: 'unknown', explicitUnknown: true };
  if (status !== 'confirmed') return { ...out, status: 'unknown' };
  if (signature === null || out.notSent || out.expired) {
    return {
      ...out,
      status: 'unknown',
      rejectMessage: 'Confirmed response lacks a valid signature or contradicts notSent/expired evidence',
    };
  }
  if (out.slot === null)
    return { ...out, status: 'unknown', rejectMessage: 'Confirmed response is missing its transaction slot' };
  const { cpi, problem } = decodeCpi(b.cpi, opts.side ?? null);
  out.cpi = cpi;
  out.cpiProblem =
    b.cpi === null || b.cpi === undefined
      ? typeof b.cpiError === 'string'
        ? `Missing CPI: ${b.cpiError}`
        : null
      : problem;
  if (b.cancel !== undefined) out.cancel = decodeCancel(b.cancel);
  return { ...out, status: 'confirmed' };
}
All files