rwa-kyc-hook-api 0.2.0

Token-2022 KYC Transfer Hook for RWA primary issuance on x402
Documentation
use solana_program::pubkey::Pubkey;
use steel::*;

#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum HookInstruction {
    CreateGlobalKycRecord = 0,
    CreateOfferingKycRecord = 1,
    UpdateGlobalKycVerified = 2,
    UpdateOfferingKycVerified = 3,
    RegisterMint = 4,
    RotateOpsAuthority = 5,
    UpdateIssuerStatus = 6,
    UpdateIssuerIdentity = 7,

    Initialize = 100,
    UpdatePlatformConfig = 101,
    RegisterIssuer = 102,
    UpdatePlatformAdmin = 103,
    AcceptPlatformAdmin = 104,
    CancelPlatformAdminProposal = 105,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {
    pub platform_admin: Pubkey,
    pub fee_recipient: Pubkey,
    /// little-endian u64; byte array keeps the struct 1-byte aligned so
    /// `try_from_bytes` works after the 1-byte instruction opcode is stripped.
    pub issuer_registration_fee_lamports: [u8; 8],
    pub cluster: u8,
    pub registration_mode: u8,
    pub _padding: [u8; 6],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdatePlatformConfig {
    pub fee_recipient: Pubkey,
    /// little-endian u64 (see `Initialize::issuer_registration_fee_lamports`).
    pub issuer_registration_fee_lamports: [u8; 8],
    pub registration_mode: u8,
    pub paused: u8,
    pub _padding: [u8; 6],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct RegisterIssuer {
    pub issuer_id: [u8; 16],
    pub identity: Pubkey,
    pub ops_authority: Pubkey,
    pub registration_path: u8,
    pub _padding: [u8; 7],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct CreateGlobalKycRecord {
    pub user: Pubkey,
    pub issuer_id: [u8; 16],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct CreateOfferingKycRecord {
    pub user: Pubkey,
    pub issuer_id: [u8; 16],
    pub offering_id: [u8; 32],
    pub offering_id_len: u8,
    pub _padding: [u8; 7],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdateGlobalKycVerified {
    pub user: Pubkey,
    pub issuer_id: [u8; 16],
    pub is_kyc_verified: u8,
    pub _padding: [u8; 7],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdateOfferingKycVerified {
    pub user: Pubkey,
    pub issuer_id: [u8; 16],
    pub offering_id: [u8; 32],
    pub offering_id_len: u8,
    pub is_kyc_verified: u8,
    pub _padding: [u8; 6],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct RegisterMint {
    pub mint: Pubkey,
    pub issuer_id: [u8; 16],
    pub offering_id: [u8; 32],
    pub offering_id_len: u8,
    pub kyc_policy: u8,
    pub _padding: [u8; 6],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct RotateOpsAuthority {
    pub issuer_id: [u8; 16],
    pub new_ops_authority: Pubkey,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdateIssuerStatus {
    pub issuer_id: [u8; 16],
    pub status: u8,
    pub _padding: [u8; 7],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdateIssuerIdentity {
    pub issuer_id: [u8; 16],
    pub new_identity: Pubkey,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdatePlatformAdmin {
    pub new_admin: Pubkey,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct AcceptPlatformAdmin {}

#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct CancelPlatformAdminProposal {}

instruction!(HookInstruction, Initialize);
instruction!(HookInstruction, UpdatePlatformConfig);
instruction!(HookInstruction, RegisterIssuer);
instruction!(HookInstruction, CreateGlobalKycRecord);
instruction!(HookInstruction, CreateOfferingKycRecord);
instruction!(HookInstruction, UpdateGlobalKycVerified);
instruction!(HookInstruction, UpdateOfferingKycVerified);
instruction!(HookInstruction, RegisterMint);
instruction!(HookInstruction, RotateOpsAuthority);
instruction!(HookInstruction, UpdateIssuerStatus);
instruction!(HookInstruction, UpdateIssuerIdentity);
instruction!(HookInstruction, UpdatePlatformAdmin);
instruction!(HookInstruction, AcceptPlatformAdmin);
instruction!(HookInstruction, CancelPlatformAdminProposal);

#[cfg(test)]
mod layout_tests {
    use super::*;

    // Steel's `instruction!` prepends a 1-byte opcode, so the program parses the
    // struct body from a 1-byte-offset slice. `bytemuck::try_from_bytes` enforces the
    // struct's alignment, so EVERY instruction struct must be 1-byte aligned (no bare
    // u16/u32/u64/i64 fields — use [u8; N] instead). This caught the real Initialize
    // bug ("invalid instruction data" at parse on-chain). Guard it here.
    macro_rules! assert_one_byte_aligned {
        ($($t:ty),+ $(,)?) => {$(
            assert_eq!(
                core::mem::align_of::<$t>(), 1,
                "{} must be 1-byte aligned for opcode-prefixed instruction parsing",
                stringify!($t)
            );
        )+};
    }

    #[test]
    fn instruction_structs_are_one_byte_aligned() {
        assert_one_byte_aligned!(
            Initialize,
            UpdatePlatformConfig,
            RegisterIssuer,
            CreateGlobalKycRecord,
            CreateOfferingKycRecord,
            UpdateGlobalKycVerified,
            UpdateOfferingKycVerified,
            RegisterMint,
            RotateOpsAuthority,
            UpdateIssuerStatus,
            UpdateIssuerIdentity,
            UpdatePlatformAdmin,
            AcceptPlatformAdmin,
            CancelPlatformAdminProposal,
        );
    }

    #[test]
    fn initialize_round_trips_through_opcode_prefixed_bytes() {
        let ix = Initialize {
            platform_admin: Pubkey::new_unique(),
            fee_recipient: Pubkey::new_unique(),
            issuer_registration_fee_lamports: 1_234_567_890u64.to_le_bytes(),
            cluster: 1,
            registration_mode: 2,
            _padding: [0; 6],
        };
        // to_bytes() prepends the 1-byte opcode; try_from_bytes() parses the rest.
        let bytes = ix.to_bytes();
        assert_eq!(bytes[0], HookInstruction::Initialize as u8);
        let parsed = Initialize::try_from_bytes(&bytes[1..]).expect("parse");
        assert_eq!(parsed.platform_admin, ix.platform_admin);
        assert_eq!(
            u64::from_le_bytes(parsed.issuer_registration_fee_lamports),
            1_234_567_890
        );
        assert_eq!(parsed.cluster, 1);
    }
}