src/binding/check.ts
v0.1.0 · 3.7 KB
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,
};
}