Skip to main content

tengu_api/
utils.rs

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