Skip to main content

tengu_api/
utils.rs

1//! Account validation and shared program utilities.
2
3use solana_program::account_info::AccountInfo;
4use solana_program::ed25519_program;
5use solana_program::pubkey::Pubkey;
6use solana_program::instruction::Instruction;
7use solana_program::program_error::ProgramError;
8use solana_program::sysvar::instructions::{load_current_index_checked, load_instruction_at_checked};
9
10use crate::consts::{
11    ADMIN_ADDRESS, CLAIM_TASK_PREFIX, ED25519_DATA_START, ED25519_PUBKEY_SIZE, ED25519_SIGNATURE_SIZE,
12    TASK_VERIFIER,
13};
14use crate::error::DojosError;
15
16/// Build ed25519 verify instruction for top-level tx (not CPI - Ed25519 doesn't support CPI).
17/// Client must prepend this instruction before claim_daily_reward.
18pub fn new_ed25519_instruction_with_signature(
19    message: &[u8],
20    signature: &[u8; 64],
21    pubkey: &[u8; 32],
22) -> Instruction {
23    let public_key_offset = ED25519_DATA_START;
24    let signature_offset = public_key_offset + ED25519_PUBKEY_SIZE;
25    let message_data_offset = signature_offset + ED25519_SIGNATURE_SIZE;
26
27    let mut data = Vec::with_capacity(message_data_offset + message.len());
28    data.extend_from_slice(&[1u8, 0u8]); // num_signatures, padding
29    data.extend_from_slice(&(signature_offset as u16).to_le_bytes());
30    data.extend_from_slice(&u16::MAX.to_le_bytes()); // signature_instruction_index
31    data.extend_from_slice(&(public_key_offset as u16).to_le_bytes());
32    data.extend_from_slice(&u16::MAX.to_le_bytes()); // public_key_instruction_index
33    data.extend_from_slice(&(message_data_offset as u16).to_le_bytes());
34    data.extend_from_slice(&(message.len() as u16).to_le_bytes());
35    data.extend_from_slice(&u16::MAX.to_le_bytes()); // message_instruction_index
36    data.extend_from_slice(pubkey);
37    data.extend_from_slice(signature);
38    data.extend_from_slice(message);
39
40    Instruction {
41        program_id: ed25519_program::id(),
42        accounts: vec![],
43        data,
44    }
45}
46
47/// Verify Ed25519 signature via instruction introspection. The client must prepend the
48/// Ed25519 verify instruction before claim_daily_reward (Ed25519 cannot be called via CPI).
49/// Message: prefix + dojo_pda + task_id. Always 1 day per claim.
50pub fn verify_signed_task_via_introspection(
51    dojo_pda: Pubkey,
52    task_id: u64,
53    signature: &[u8; 64],
54    instructions_sysvar_info: &AccountInfo,
55) -> Result<(), ProgramError> {
56    let current_index = load_current_index_checked(instructions_sysvar_info)?;
57    let prev_index = current_index.checked_sub(1).ok_or(ProgramError::InvalidInstructionData)?;
58    let prev_ix = load_instruction_at_checked(prev_index as usize, instructions_sysvar_info)?;
59
60    if prev_ix.program_id != ed25519_program::id() {
61        return Err(ProgramError::InvalidInstructionData);
62    }
63
64    let data = &prev_ix.data;
65    if data.len() < ED25519_DATA_START + ED25519_PUBKEY_SIZE + ED25519_SIGNATURE_SIZE {
66        return Err(ProgramError::InvalidInstructionData);
67    }
68
69    let verifier_bytes: [u8; 32] = TASK_VERIFIER.to_bytes();
70    if data[ED25519_DATA_START..ED25519_DATA_START + ED25519_PUBKEY_SIZE] != verifier_bytes[..] {
71        return Err(ProgramError::InvalidInstructionData);
72    }
73    let sig_start = ED25519_DATA_START + ED25519_PUBKEY_SIZE;
74    if data[sig_start..sig_start + ED25519_SIGNATURE_SIZE] != signature[..] {
75        return Err(ProgramError::InvalidInstructionData);
76    }
77
78    let mut expected_message = Vec::with_capacity(CLAIM_TASK_PREFIX.len() + 32 + 8);
79    expected_message.extend_from_slice(CLAIM_TASK_PREFIX);
80    expected_message.extend_from_slice(dojo_pda.as_ref());
81    expected_message.extend_from_slice(&task_id.to_le_bytes());
82
83    let msg_start = sig_start + ED25519_SIGNATURE_SIZE;
84    if data.len() < msg_start + expected_message.len() || data[msg_start..msg_start + expected_message.len()] != expected_message[..] {
85        return Err(ProgramError::InvalidInstructionData);
86    }
87
88    Ok(())
89}
90
91/// Asserts that an account's key matches the expected pubkey.
92pub fn assert_key(info: &AccountInfo, expected: &Pubkey) -> Result<(), ProgramError> {
93    if info.key != expected {
94        return Err(ProgramError::InvalidAccountData);
95    }
96    Ok(())
97}
98
99/// Asserts that the authority matches ADMIN_ADDRESS.
100pub fn assert_admin(authority: &AccountInfo) -> Result<(), ProgramError> {
101    if authority.key != &ADMIN_ADDRESS {
102        return Err(DojosError::UnauthorizedAdmin.into());
103    }
104    Ok(())
105}
106