98 lines
3.4 KiB
Rust
Raw Normal View History

pub use nssa_core::program::PdaSeed;
use nssa_core::{
account::{AccountId, AccountWithMetadata},
program::ProgramId,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub enum Instruction {
/// Create the Associated Token Account for (token program, owner, definition).
/// Idempotent: no-op if the account already exists.
///
/// Required accounts (3):
/// - Owner account
/// - Token definition account
/// - Associated token account (default/uninitialized, or already initialized)
///
/// `token_program_id` is explicit so callers can support multiple token programs without
/// letting account metadata choose downstream code.
Create { token_program_id: ProgramId },
/// Transfer tokens FROM owner's ATA to a recipient token holding account.
/// Uses ATA PDA seeds to authorize the chained Token::Transfer call.
///
/// Required accounts (3):
/// - Owner account (authorized)
/// - Sender ATA (owner's token holding)
/// - Recipient token holding. Must be:
/// - already initialized (not a default account),
/// - owned by the same token program as the sender ATA,
/// - and point at the same token definition as the sender.
///
/// `token_program_id` is explicit so callers can support multiple token programs without
/// letting account metadata choose downstream code.
Transfer {
token_program_id: ProgramId,
amount: u128,
},
/// Burn tokens FROM owner's ATA.
/// Uses PDA seeds to authorize the ATA in the chained Token::Burn call.
///
/// Required accounts (3):
/// - Owner account (authorized)
/// - Owner's ATA (the holding to burn from)
/// - Token definition account
///
/// `token_program_id` is explicit so callers can support multiple token programs without
/// letting account metadata choose downstream code.
Burn {
token_program_id: ProgramId,
amount: u128,
},
}
pub fn compute_ata_seed(
token_program_id: ProgramId,
owner_id: AccountId,
definition_id: AccountId,
) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256};
let mut bytes = [0u8; 96];
for (index, word) in token_program_id.iter().enumerate() {
let offset = index * 4;
bytes[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
bytes[32..64].copy_from_slice(&owner_id.to_bytes());
bytes[64..96].copy_from_slice(&definition_id.to_bytes());
PdaSeed::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
pub fn get_associated_token_account_id(ata_program_id: &ProgramId, seed: &PdaSeed) -> AccountId {
2026-05-11 15:29:41 +02:00
AccountId::for_public_pda(ata_program_id, seed)
}
/// Verify the ATA's address matches `(ata_program_id, token_program_id, owner, definition)` and
/// return the [`PdaSeed`] for use in chained calls.
pub fn verify_ata_and_get_seed(
ata_account: &AccountWithMetadata,
owner: &AccountWithMetadata,
token_program_id: ProgramId,
definition_id: AccountId,
ata_program_id: ProgramId,
) -> PdaSeed {
let seed = compute_ata_seed(token_program_id, owner.account_id, definition_id);
let expected_id = get_associated_token_account_id(&ata_program_id, &seed);
assert_eq!(
ata_account.account_id, expected_id,
"ATA account ID does not match expected derivation"
);
seed
}