Skip to content
markpaper

src/onchain/header.ts

v0.1.0 · 4.2 KB

Download file
import crypto from 'node:crypto';
import { base58Encode } from '../address/base58.js';
// Public mainnet program address, rise-public SDK addresses.
export const PROGRAM_ADDRESS = 'EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih'; // privacy-allow: public Phoenix mainnet program address from rise-public
export const TRADER_HEADER = {
  AUTHORITY: 56,
  COLLATERAL: 88,
  FLAGS: 96,
  MAX_POSITIONS: 112,
  PREFS: 116,
  POSITION_AUTHORITY: 120,
  PDA_INDEX: 154,
  SUB_INDEX: 155,
  LEN: 224,
} as const;
export const TRADER_DISCRIMINANT = crypto.createHash('sha256').update('account:trader').digest().subarray(0, 8);
export interface TraderHeader {
  authority: string;
  positionAuthority: string;
  collateralQuoteLots: bigint;
  flags: number;
  maxPositions: number;
  preferenceBits: number;
  pdaIndex: number;
  subaccountIndex: number;
}
export function parseTraderHeader(
  data: Uint8Array,
  owner: string | null,
):
  | {
      ok: true;
      header: TraderHeader;
    }
  | {
      ok: false;
      why: string;
    } {
  if (owner !== PROGRAM_ADDRESS)
    return { ok: false, why: `Account owner ${owner ?? 'absent'} differs from Phoenix program ${PROGRAM_ADDRESS}` };
  if (!(data instanceof Uint8Array) || data.length < TRADER_HEADER.LEN)
    return { ok: false, why: `Trader header requires ${TRADER_HEADER.LEN} bytes; received ${data?.length ?? 0}` };
  if (!Buffer.from(data.subarray(0, 8)).equals(TRADER_DISCRIMINANT))
    return { ok: false, why: 'Invalid account:trader discriminator' };
  const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
  return {
    ok: true,
    header: {
      authority: base58Encode(data.subarray(TRADER_HEADER.AUTHORITY, TRADER_HEADER.AUTHORITY + 32)),
      positionAuthority: base58Encode(
        data.subarray(TRADER_HEADER.POSITION_AUTHORITY, TRADER_HEADER.POSITION_AUTHORITY + 32),
      ),
      collateralQuoteLots: dv.getBigInt64(TRADER_HEADER.COLLATERAL, true),
      flags: dv.getUint32(TRADER_HEADER.FLAGS, true),
      maxPositions: dv.getUint32(TRADER_HEADER.MAX_POSITIONS, true),
      preferenceBits: dv.getUint32(TRADER_HEADER.PREFS, true),
      pdaIndex: data[TRADER_HEADER.PDA_INDEX]!,
      subaccountIndex: data[TRADER_HEADER.SUB_INDEX]!,
    },
  };
}
export function isFrozen(flags: number): boolean {
  const has = (m: number) => (flags & m) === m;
  return has(4) && !has(8) && !has(32) && !has(16);
}
export interface TraderCapabilities {
  flags: number;
  state:
    | 'uninitialized'
    | 'frozen_hot'
    | 'frozen_cold'
    | 'reduce_only_hot'
    | 'reduce_only_cold'
    | 'hot_active'
    | 'cold';
  hot: boolean;
  placeLimitOrder: boolean;
  placeMarketOrder: boolean;
  riskIncreasingTrade: boolean;
  riskReducingTrade: boolean;
  depositCollateral: boolean;
  withdrawCollateral: boolean;
}
export function describeCapabilities(flags: number): TraderCapabilities {
  const has = (m: number) => (flags & m) === m;
  const hot = has(1);
  const frozen = isFrozen(flags);
  const reduceOnly = has(4) && !has(8) && has(32) && has(16);
  const state: TraderCapabilities['state'] =
    flags === 0
      ? 'uninitialized'
      : frozen
        ? hot
          ? 'frozen_hot'
          : 'frozen_cold'
        : reduceOnly
          ? hot
            ? 'reduce_only_hot'
            : 'reduce_only_cold'
          : hot
            ? 'hot_active'
            : 'cold';
  return {
    flags,
    state,
    hot,
    placeLimitOrder: has(2),
    placeMarketOrder: has(4),
    riskIncreasingTrade: has(4) && has(8),
    riskReducingTrade: has(4),
    depositCollateral: has(16),
    withdrawCollateral: has(32),
  };
}
export function traderRestrictions(c: TraderCapabilities): string[] {
  const r: string[] = [];
  if (c.state === 'uninitialized') return r;
  if (c.state.startsWith('frozen')) r.push('Account frozen');
  else if (c.state.startsWith('reduce_only')) r.push('Account reduce-only');
  if (!c.riskIncreasingTrade) r.push('Risk-increasing trades disabled');
  if (!c.placeMarketOrder) r.push('Market orders disabled');
  if (!c.placeLimitOrder) r.push('Limit orders disabled');
  if (!c.withdrawCollateral) r.push('Collateral withdrawals disabled');
  if (!c.depositCollateral) r.push('Collateral deposits disabled');
  return r;
}
All files