src/onchain/header.ts
v0.1.0 · 4.2 KB
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;
}