src/signer/decode.ts
v0.1.0 · 6.4 KB
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' };
}