Skip to content
markpaper

src/binding/check.ts

v0.1.0 · 3.7 KB

Download file
import { isBase58Pubkey } from '../address/base58.js';
import { parseTraderHeader, type TraderHeader } from '../onchain/header.js';
import type { SignerClient } from '../signer/client.js';
import type { SidecarHealthView } from '../signer/health.js';

export interface BindingExpect {
  authority: string;
  delegate: string;
  traderPda: string;
}
export type BindingVerdict = 'ok' | 'lost' | 'unknown';
export function headerConfigProblems(h: TraderHeader, e: BindingExpect): string[] {
  const p: string[] = [];
  if (h.authority !== e.authority) p.push('On-chain authority differs from expected owner');
  if (h.pdaIndex !== 0 || h.subaccountIndex !== 0) p.push('Account indices are not 0/0');
  return p;
}
export function headerDelegationProblems(h: TraderHeader, e: BindingExpect): string[] {
  const p: string[] = [];
  if (h.positionAuthority !== e.delegate)
    p.push(
      h.positionAuthority === h.authority ? 'Delegation absent' : 'Position authority differs from expected delegate',
    );
  if (h.flags === 0) p.push('Account uninitialized');
  return p;
}
export function headerProblems(h: TraderHeader, e: BindingExpect): string[] {
  return [...headerConfigProblems(h, e), ...headerDelegationProblems(h, e)];
}
export function signerIdentityProblems(h: SidecarHealthView, e: BindingExpect): string[] {
  const p: string[] = [];
  if (h.pubkey !== e.delegate) p.push('Signer public key differs from expected delegate');
  if (h.authority !== e.authority) p.push('Signer authority differs from expected owner');
  if (h.traderPda !== e.traderPda) p.push('Signer PDA differs from expected account');
  if (h.traderPdaIndex !== 0 || h.subaccountIndex !== 0) p.push('Signer indices are not 0/0');
  return p;
}
export function configProblems(e: BindingExpect): string[] {
  return Object.entries(e)
    .filter(([, v]) => !isBase58Pubkey(v))
    .map(([k]) => `Invalid canonical public key: ${k}`);
}
export async function checkBinding(opts: { signer: Pick<SignerClient, 'get' | 'health'>; expect: BindingExpect }) {
  const cfg = configProblems(opts.expect);
  if (cfg.length)
    return { available: false, verdict: 'lost' as BindingVerdict, problems: cfg, health: null, header: null };
  const health = await opts.signer.health();
  const r = await opts.signer.get(`/account/${encodeURIComponent(opts.expect.traderPda)}?offset=0&length=224`);
  let header: TraderHeader | null = null;
  let parseError: string | null = null;
  const body = r.body as { exists?: unknown; owner?: unknown; dataBase64?: unknown } | null;
  if (r.reachable && r.httpStatus === 200 && body?.exists === true && typeof body.dataBase64 === 'string') {
    const p = parseTraderHeader(
      new Uint8Array(Buffer.from(body.dataBase64, 'base64')),
      typeof body.owner === 'string' ? body.owner : null,
    );
    if (p.ok) header = p.header;
    else parseError = p.why;
  }
  const probs = [
    ...(health.health ? signerIdentityProblems(health.health, opts.expect) : []),
    ...(header ? headerProblems(header, opts.expect) : parseError ? [parseError] : []),
  ];
  const available = header !== null && health.health !== null;
  const verdict: BindingVerdict = probs.length ? 'lost' : available ? 'ok' : 'unknown';
  return { available, verdict, problems: probs, health: health.health, header };
}
export function createBindingGate() {
  let verdict: BindingVerdict = 'unknown';
  let problems: string[] = [];
  return {
    note(v: BindingVerdict, p: string[] = []) {
      if (v === 'unknown') return;
      verdict = v;
      problems = v === 'lost' ? [...p] : [];
    },
    writeBlock(): string | null {
      return verdict === 'unknown'
        ? 'Delegation not yet verified'
        : verdict === 'lost'
          ? `Delegation lost: ${problems.join('; ')}`
          : null;
    },
    verdict: () => verdict,
  };
}
All files