src/onchain/global.ts
v0.1.0 · 5.4 KB
import { createHash } from 'node:crypto';
import { base58Encode } from '../address/base58.js';
import { PROGRAM_ADDRESS } from './header.js';
const sha256 = (s: string) => createHash('sha256').update(s).digest();
export const GLOBAL_CONFIGURATION = '2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ'; // privacy-allow: public Phoenix mainnet global configuration, rise-public
export const LOG_AUTHORITY = 'GdxfTLSsdSY37G6fZoYtdGDSfgFnbT2EmRpuePZxWShS'; // privacy-allow: public Phoenix mainnet log authority, rise-public
export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111'; // privacy-allow: public Solana compute budget program
export const MAX_TX_BYTES = 1232; // Solana transaction wire size ceiling
export const MAX_CANCEL_IDS = 30; // Conservative validated ceiling; actual wire size is checked too
export const MAX_LIMIT_ORDERS_PER_SIDE = 64; // rise 0.5.28 scaleOrders.ts
export interface ExchangeKeys {
globalConfig: string;
perpAssetMap: string;
globalTraderIndex: string[];
activeTraderBuffer: string[];
}
export const PREF_DISABLE_COLLATERAL_SWEEP = 1 << 0;
export const PREF_DISABLE_POSITION_AUTHORITY_SWAP = 1 << 1;
const SPOT_FLAG_IS_ACTIVE = 1 << 0;
const SPOT_FLAG_DISABLE_POSITION_AUTHORITY_SWAP = 1 << 2;
export function describePreferences(bits: number) {
const b = Number(bits) >>> 0;
return {
bits: b,
disableCollateralSweep: (b & PREF_DISABLE_COLLATERAL_SWEEP) !== 0,
disablePositionAuthoritySwap: (b & PREF_DISABLE_POSITION_AUTHORITY_SWAP) !== 0,
reservedBits: b & ~(PREF_DISABLE_COLLATERAL_SWEEP | PREF_DISABLE_POSITION_AUTHORITY_SWAP),
};
}
export const GLOBAL_CONFIG_DISCRIMINANT = sha256('account:global_configuration').subarray(0, 8);
export const GLOBAL_CONFIG_LAYOUT = {
ACCOUNT_KEY: 8,
PERP_ASSET_MAP: 360,
GTI_HEADER: 392,
ATB_HEADER: 424,
QUOTE_DECIMALS: 505,
SPOT_META: 776,
SPOT_FLAGS: 776 + 104,
LEN: 1104,
};
export function parseGlobalConfig(data: Uint8Array, owner: string | null) {
if (owner !== PROGRAM_ADDRESS)
return { ok: false, why: `GlobalConfig owner ${owner ?? 'missing'} differs from mainnet program` };
if (!(data instanceof Uint8Array) || data.length < GLOBAL_CONFIG_LAYOUT.LEN)
return {
ok: false,
why: `Global configuration requires ${GLOBAL_CONFIG_LAYOUT.LEN} bytes; received ${data?.length ?? 0}`,
};
if (!Buffer.from(data.subarray(0, 8)).equals(GLOBAL_CONFIG_DISCRIMINANT))
return { ok: false, why: 'Invalid account:global_configuration discriminator' };
const key = (o: number) => base58Encode(data.subarray(o, o + 32));
const spotFlags = data[GLOBAL_CONFIG_LAYOUT.SPOT_FLAGS]!;
return {
ok: true,
accountKey: key(GLOBAL_CONFIG_LAYOUT.ACCOUNT_KEY),
perpAssetMap: key(GLOBAL_CONFIG_LAYOUT.PERP_ASSET_MAP),
globalTraderIndexHeader: key(GLOBAL_CONFIG_LAYOUT.GTI_HEADER),
activeTraderBufferHeader: key(GLOBAL_CONFIG_LAYOUT.ATB_HEADER),
quoteDecimals: data[GLOBAL_CONFIG_LAYOUT.QUOTE_DECIMALS],
nativeSol: {
flags: spotFlags,
active: (spotFlags & SPOT_FLAG_IS_ACTIVE) !== 0,
positionAuthoritySwapDisabled: (spotFlags & SPOT_FLAG_DISABLE_POSITION_AUTHORITY_SWAP) !== 0,
},
};
}
export function checkViewKeysAgainstChain(keys: ExchangeKeys, gc: ReturnType<typeof parseGlobalConfig>) {
const p: string[] = [];
if (!gc?.ok) return [gc?.why ?? 'Global configuration unavailable'];
if (gc.accountKey !== GLOBAL_CONFIGURATION)
p.push(`GlobalConfig.account_key ${gc.accountKey} ≠ ${GLOBAL_CONFIGURATION}`);
if (keys.perpAssetMap !== gc.perpAssetMap)
p.push(`REST perpAssetMap ${keys.perpAssetMap} differs from on-chain key ${gc.perpAssetMap}`);
if (keys.globalTraderIndex[0] !== gc.globalTraderIndexHeader)
p.push(
`REST globalTraderIndex[0] ${keys.globalTraderIndex[0]} differs from on-chain key ${gc.globalTraderIndexHeader}`,
);
if (keys.activeTraderBuffer[0] !== gc.activeTraderBufferHeader)
p.push(
`REST activeTraderBuffer[0] ${keys.activeTraderBuffer[0]} differs from on-chain key ${gc.activeTraderBufferHeader}`,
);
if (gc.quoteDecimals !== 6) p.push(`Quote decimals ${gc.quoteDecimals} differs from the supported protocol scale 6`);
return p;
}
/** @experimental SwapNative semantics follow rise-public Rust source; the attack path has not been verified. */
export function keyRiskOf(
prefs: ReturnType<typeof describePreferences> | null,
gc: ReturnType<typeof parseGlobalConfig> | null,
) {
const traderOptOut = prefs ? prefs.disablePositionAuthoritySwap : null;
const exchangeKill = gc?.ok && gc.nativeSol ? gc.nativeSol.positionAuthoritySwapDisabled : null;
const featureActive = gc?.ok && gc.nativeSol ? gc.nativeSol.active : null;
let positionAuthoritySwap:
| 'closed_by_trader'
| 'closed_by_exchange'
| 'unknown'
| 'open'
| 'open_when_feature_active';
if (traderOptOut === true) positionAuthoritySwap = 'closed_by_trader';
else if (exchangeKill === true) positionAuthoritySwap = 'closed_by_exchange';
else if (traderOptOut === null || exchangeKill === null) positionAuthoritySwap = 'unknown';
else positionAuthoritySwap = featureActive ? 'open' : 'open_when_feature_active';
const warning =
positionAuthoritySwap === 'open' || positionAuthoritySwap === 'open_when_feature_active'
? 'position_authority_swap_enabled'
: null;
return {
positionAuthoritySwap,
warning,
traderOptOut,
exchangeKillSwitch: exchangeKill,
nativeSolCollateralActive: featureActive,
};
}