gmsol-sdk 0.9.0

GMX-Solana is an extension of GMX on the Solana blockchain.
Documentation
use libsecp256k1::{recover, Message as SecpMessage, RecoveryId, Signature};
use solana_sdk::{keccak, pubkey::Pubkey};

use super::types::{DecimalsMap, EncodedRecommendation, ValuesMap};

fn pad_utf8<const N: usize>(s: &str) -> [u8; N] {
    let mut out = [0u8; N];
    let bytes = s.as_bytes();
    let len = bytes.len().min(N);
    out[..len].copy_from_slice(&bytes[..len]);
    out
}

fn parse_uuid_16(ref_id: &str) -> crate::Result<[u8; 16]> {
    let v = uuid::Uuid::parse_str(ref_id).map_err(crate::Error::custom)?;
    Ok(*v.as_bytes())
}

fn push_kv(
    msg: &mut Vec<u8>,
    values: &ValuesMap,
    decimals: &DecimalsMap,
    key: &str,
) -> crate::Result<()> {
    let v = *values
        .get(key)
        .ok_or_else(|| crate::Error::custom(format!("missing value for {key}")))?;
    let d = *decimals
        .get(key)
        .ok_or_else(|| crate::Error::custom(format!("missing decimals for {key}")))?;
    msg.extend_from_slice(&v.to_le_bytes());
    msg.push(d);
    Ok(())
}

pub fn build_signed_message(rec: &EncodedRecommendation) -> crate::Result<[u8; 32]> {
    let mut msg = Vec::new();

    msg.extend_from_slice(&pad_utf8::<32>(&rec.parameter_name));

    let market = rec.market_pubkey()?;
    msg.extend_from_slice(&market.to_bytes());

    match rec.parameter_name.as_str() {
        "oiCaps" => {
            push_kv(
                &mut msg,
                &rec.new_values,
                &rec.decimals,
                "oiCaps/maxOpenInterestForLongs/v1",
            )?;
            push_kv(
                &mut msg,
                &rec.new_values,
                &rec.decimals,
                "oiCaps/maxOpenInterestForShorts/v1",
            )?;
        }
        "priceImpact" => {
            push_kv(
                &mut msg,
                &rec.new_values,
                &rec.decimals,
                "priceImpact/negativePositionImpactFactor/v1",
            )?;
            push_kv(
                &mut msg,
                &rec.new_values,
                &rec.decimals,
                "priceImpact/positionImpactExponentFactor/v1",
            )?;
            push_kv(
                &mut msg,
                &rec.new_values,
                &rec.decimals,
                "priceImpact/positivePositionImpactFactor/v1",
            )?;
        }
        _ => return Err(crate::Error::custom("unsupported parameter_name")),
    }

    msg.extend_from_slice(&rec.timestamp.to_le_bytes());

    msg.extend_from_slice(&pad_utf8::<16>(&rec.protocol));

    let ref_id = parse_uuid_16(&rec.reference_id)?;
    msg.extend_from_slice(&ref_id);

    let hash = keccak::hash(&msg);
    Ok(hash.to_bytes())
}

pub fn verify_signature(
    rec: &EncodedRecommendation,
    expected_signer: &Pubkey,
) -> crate::Result<()> {
    let hash = build_signed_message(rec)?;

    let mut sig_bytes = [0u8; 64];
    let sig_str = rec.signature.trim();
    let sig_hex = sig_str
        .strip_prefix("0x")
        .or_else(|| sig_str.strip_prefix("0X"))
        .unwrap_or(sig_str);
    let sig_vec = hex::decode(sig_hex).map_err(crate::Error::custom)?;
    if sig_vec.len() != 64 {
        return Err(crate::Error::custom(
            "invalid signature length; expected 64 bytes",
        ));
    }
    sig_bytes.copy_from_slice(&sig_vec);

    let rid = RecoveryId::parse(rec.recovery_id)
        .map_err(|_| crate::Error::custom("invalid recovery id"))?;

    let msg = SecpMessage::parse(&hash);
    let sig = Signature::parse_standard(&sig_bytes)
        .map_err(|_| crate::Error::custom("invalid signature format"))?;
    let pk = recover(&msg, &sig, &rid)
        .map_err(|_| crate::Error::custom("secp256k1 public key recovery failed"))?;

    let compressed = pk.serialize_compressed();
    let sol_hash = keccak::hash(&compressed);
    let recovered = Pubkey::from(sol_hash.to_bytes());

    if &recovered != expected_signer {
        return Err(crate::Error::custom(
            "signature verification failed: unexpected signer",
        ));
    }
    Ok(())
}