use crate::account::AccountView;
use crate::address::Address;
use crate::error::ProgramError;
use crate::instruction::{InstructionAccount, Signer};
use crate::token::{Invoke, TokenInstruction, TokenSink, TOKEN_2022_PROGRAM_ID};
use crate::ProgramResult;
pub const MAX_METADATA_INSTRUCTION_DATA: usize = 512;
pub const DISC_METADATA_INITIALIZE: [u8; 8] = [210, 225, 30, 162, 88, 184, 77, 141];
pub const DISC_METADATA_UPDATE_FIELD: [u8; 8] = [221, 233, 49, 45, 181, 202, 220, 200];
pub const DISC_METADATA_REMOVE_KEY: [u8; 8] = [234, 18, 32, 56, 89, 141, 37, 181];
pub const DISC_METADATA_UPDATE_AUTHORITY: [u8; 8] = [215, 228, 166, 228, 84, 100, 86, 123];
pub const DISC_METADATA_EMIT: [u8; 8] = [250, 166, 180, 250, 13, 12, 184, 70];
pub const DISC_GROUP_INITIALIZE: [u8; 8] = [121, 113, 108, 39, 54, 51, 0, 4];
pub const DISC_GROUP_UPDATE_MAX_SIZE: [u8; 8] = [108, 37, 171, 143, 248, 30, 18, 110];
pub const DISC_GROUP_UPDATE_AUTHORITY: [u8; 8] = [161, 105, 88, 1, 237, 221, 216, 203];
pub const DISC_GROUP_INITIALIZE_MEMBER: [u8; 8] = [152, 32, 222, 176, 223, 237, 116, 134];
pub struct Data {
buf: [u8; MAX_METADATA_INSTRUCTION_DATA],
len: usize,
}
impl Data {
#[inline(always)]
fn new(discriminator: &[u8; 8]) -> Self {
let mut buf = [0u8; MAX_METADATA_INSTRUCTION_DATA];
buf[..8].copy_from_slice(discriminator);
Self { buf, len: 8 }
}
#[inline(always)]
fn bytes(&mut self, bytes: &[u8]) -> ProgramResult {
let end = self
.len
.checked_add(bytes.len())
.ok_or(ProgramError::InvalidArgument)?;
if end > MAX_METADATA_INSTRUCTION_DATA {
return Err(ProgramError::InvalidArgument);
}
self.buf[self.len..end].copy_from_slice(bytes);
self.len = end;
Ok(())
}
#[inline(always)]
fn string(&mut self, text: &str) -> ProgramResult {
let len = u32::try_from(text.len()).map_err(|_| ProgramError::InvalidArgument)?;
self.bytes(&len.to_le_bytes())?;
self.bytes(text.as_bytes())
}
#[inline(always)]
fn nullable(&mut self, address: Option<&Address>) -> ProgramResult {
match address {
Some(address) if address.as_array() == &[0u8; 32] => Err(ProgramError::InvalidArgument),
Some(address) => self.bytes(address.as_array()),
None => self.bytes(&[0u8; 32]),
}
}
#[inline(always)]
fn option_u64(&mut self, value: Option<u64>) -> ProgramResult {
match value {
Some(value) => {
self.bytes(&[1])?;
self.bytes(&value.to_le_bytes())
}
None => self.bytes(&[0]),
}
}
#[inline(always)]
pub fn as_slice(&self) -> &[u8] {
&self.buf[..self.len]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MetadataField<'a> {
Name,
Symbol,
Uri,
Key(&'a str),
}
pub mod encoders {
use super::*;
#[inline(always)]
pub fn encode_initialize(name: &str, symbol: &str, uri: &str) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_METADATA_INITIALIZE);
data.string(name)?;
data.string(symbol)?;
data.string(uri)?;
Ok(data)
}
#[inline(always)]
pub fn encode_update_field(
field: MetadataField<'_>,
value: &str,
) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_METADATA_UPDATE_FIELD);
match field {
MetadataField::Name => data.bytes(&[0])?,
MetadataField::Symbol => data.bytes(&[1])?,
MetadataField::Uri => data.bytes(&[2])?,
MetadataField::Key(key) => {
data.bytes(&[3])?;
data.string(key)?;
}
}
data.string(value)?;
Ok(data)
}
#[inline(always)]
pub fn encode_remove_key(idempotent: bool, key: &str) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_METADATA_REMOVE_KEY);
data.bytes(&[u8::from(idempotent)])?;
data.string(key)?;
Ok(data)
}
#[inline(always)]
pub fn encode_update_authority(
discriminator: &[u8; 8],
new_authority: Option<&Address>,
) -> Result<Data, ProgramError> {
let mut data = Data::new(discriminator);
data.nullable(new_authority)?;
Ok(data)
}
#[inline(always)]
pub fn encode_emit(start: Option<u64>, end: Option<u64>) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_METADATA_EMIT);
data.option_u64(start)?;
data.option_u64(end)?;
Ok(data)
}
#[inline(always)]
pub fn encode_initialize_group(
update_authority: Option<&Address>,
max_size: u64,
) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_GROUP_INITIALIZE);
data.nullable(update_authority)?;
data.bytes(&max_size.to_le_bytes())?;
Ok(data)
}
#[inline(always)]
pub fn encode_update_group_max_size(max_size: u64) -> Result<Data, ProgramError> {
let mut data = Data::new(&DISC_GROUP_UPDATE_MAX_SIZE);
data.bytes(&max_size.to_le_bytes())?;
Ok(data)
}
#[inline(always)]
pub fn encode_initialize_member() -> Data {
Data::new(&DISC_GROUP_INITIALIZE_MEMBER)
}
}
macro_rules! interface_methods {
($name:ident $(, signer = $signer:ident)?) => {
impl $name<'_> {
#[inline]
pub fn invoke(&self) -> ProgramResult {
self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, &[])
}
#[inline]
pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, signers)
}
#[inline]
pub fn invoke_on_program(
&self,
program: &Address,
signers: &[Signer<'_, '_>],
) -> ProgramResult {
$(
if signers.is_empty() && !self.$signer.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
)?
self.emit(&[], &mut Invoke { program, signers })
}
}
};
}
pub struct InitializeTokenMetadata<'a> {
pub metadata: &'a AccountView<'a>,
pub update_authority: &'a AccountView<'a>,
pub mint: &'a AccountView<'a>,
pub mint_authority: &'a AccountView<'a>,
pub name: &'a str,
pub symbol: &'a str,
pub uri: &'a str,
}
impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenMetadata<'x> {
#[inline(always)]
fn emit(
&self,
_multisig_signers: &[&'a AccountView<'a>],
sink: &mut impl TokenSink<'a>,
) -> ProgramResult {
let data = encoders::encode_initialize(self.name, self.symbol, self.uri)?;
let accounts = [
InstructionAccount::writable(self.metadata.address()),
InstructionAccount::readonly(self.update_authority.address()),
InstructionAccount::readonly(self.mint.address()),
InstructionAccount::readonly_signer(self.mint_authority.address()),
];
let views = [
self.metadata,
self.update_authority,
self.mint,
self.mint_authority,
];
sink.emit(data.as_slice(), accounts, views, &[])
}
}
interface_methods!(InitializeTokenMetadata, signer = mint_authority);
macro_rules! authority_instruction {
($(#[$doc:meta])* $name:ident { target = $target:ident, authority = $auth:ident $(, $field:ident : $ty:ty)* $(,)? } data = |$s:ident| $data:expr;) => {
$(#[$doc])*
pub struct $name<'a> {
pub $target: &'a AccountView<'a>,
pub $auth: &'a AccountView<'a>,
$(pub $field: $ty,)*
}
impl<'a, 'x: 'a> TokenInstruction<'a> for $name<'x> {
#[inline(always)]
fn emit(
&self,
_multisig_signers: &[&'a AccountView<'a>],
sink: &mut impl TokenSink<'a>,
) -> ProgramResult {
let $s = self;
let data: Data = $data;
let accounts = [
InstructionAccount::writable(self.$target.address()),
InstructionAccount::readonly_signer(self.$auth.address()),
];
let views = [self.$target, self.$auth];
sink.emit(data.as_slice(), accounts, views, &[])
}
}
interface_methods!($name, signer = $auth);
};
}
authority_instruction! {
UpdateMetadataField { target = metadata, authority = update_authority, field: MetadataField<'a>, value: &'a str }
data = |s| encoders::encode_update_field(s.field, s.value)?;
}
authority_instruction! {
RemoveMetadataKey { target = metadata, authority = update_authority, idempotent: bool, key: &'a str }
data = |s| encoders::encode_remove_key(s.idempotent, s.key)?;
}
authority_instruction! {
UpdateMetadataAuthority { target = metadata, authority = current_authority, new_authority: Option<&'a Address> }
data = |s| encoders::encode_update_authority(&DISC_METADATA_UPDATE_AUTHORITY, s.new_authority)?;
}
pub struct EmitTokenMetadata<'a> {
pub metadata: &'a AccountView<'a>,
pub start: Option<u64>,
pub end: Option<u64>,
}
impl<'a, 'x: 'a> TokenInstruction<'a> for EmitTokenMetadata<'x> {
#[inline(always)]
fn emit(
&self,
_multisig_signers: &[&'a AccountView<'a>],
sink: &mut impl TokenSink<'a>,
) -> ProgramResult {
let data = encoders::encode_emit(self.start, self.end)?;
let accounts = [InstructionAccount::readonly(self.metadata.address())];
let views = [self.metadata];
sink.emit(data.as_slice(), accounts, views, &[])
}
}
interface_methods!(EmitTokenMetadata);
pub struct InitializeTokenGroup<'a> {
pub group: &'a AccountView<'a>,
pub mint: &'a AccountView<'a>,
pub mint_authority: &'a AccountView<'a>,
pub update_authority: Option<&'a Address>,
pub max_size: u64,
}
impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenGroup<'x> {
#[inline(always)]
fn emit(
&self,
_multisig_signers: &[&'a AccountView<'a>],
sink: &mut impl TokenSink<'a>,
) -> ProgramResult {
let data = encoders::encode_initialize_group(self.update_authority, self.max_size)?;
let accounts = [
InstructionAccount::writable(self.group.address()),
InstructionAccount::readonly(self.mint.address()),
InstructionAccount::readonly_signer(self.mint_authority.address()),
];
let views = [self.group, self.mint, self.mint_authority];
sink.emit(data.as_slice(), accounts, views, &[])
}
}
interface_methods!(InitializeTokenGroup, signer = mint_authority);
authority_instruction! {
UpdateTokenGroupMaxSize { target = group, authority = update_authority, max_size: u64 }
data = |s| encoders::encode_update_group_max_size(s.max_size)?;
}
authority_instruction! {
UpdateTokenGroupAuthority { target = group, authority = current_authority, new_authority: Option<&'a Address> }
data = |s| encoders::encode_update_authority(&DISC_GROUP_UPDATE_AUTHORITY, s.new_authority)?;
}
pub struct InitializeTokenGroupMember<'a> {
pub member: &'a AccountView<'a>,
pub member_mint: &'a AccountView<'a>,
pub member_mint_authority: &'a AccountView<'a>,
pub group: &'a AccountView<'a>,
pub group_update_authority: &'a AccountView<'a>,
}
impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenGroupMember<'x> {
#[inline(always)]
fn emit(
&self,
_multisig_signers: &[&'a AccountView<'a>],
sink: &mut impl TokenSink<'a>,
) -> ProgramResult {
let data = encoders::encode_initialize_member();
let accounts = [
InstructionAccount::writable(self.member.address()),
InstructionAccount::readonly(self.member_mint.address()),
InstructionAccount::readonly_signer(self.member_mint_authority.address()),
InstructionAccount::writable(self.group.address()),
InstructionAccount::readonly_signer(self.group_update_authority.address()),
];
let views = [
self.member,
self.member_mint,
self.member_mint_authority,
self.group,
self.group_update_authority,
];
sink.emit(data.as_slice(), accounts, views, &[])
}
}
impl InitializeTokenGroupMember<'_> {
#[inline]
pub fn invoke(&self) -> ProgramResult {
self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, &[])
}
#[inline]
pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, signers)
}
#[inline]
pub fn invoke_on_program(
&self,
program: &Address,
signers: &[Signer<'_, '_>],
) -> ProgramResult {
if signers.is_empty()
&& !(self.member_mint_authority.is_signer() && self.group_update_authority.is_signer())
{
return Err(ProgramError::MissingRequiredSignature);
}
self.emit(&[], &mut Invoke { program, signers })
}
}
#[cfg(test)]
mod tests {
use super::encoders::*;
use super::*;
#[test]
fn discriminators_are_the_hash_prefixes_of_the_interface_inputs() {
for (input, expected) in [
(
"spl_token_metadata_interface:initialize_account",
DISC_METADATA_INITIALIZE,
),
(
"spl_token_metadata_interface:updating_field",
DISC_METADATA_UPDATE_FIELD,
),
(
"spl_token_metadata_interface:remove_key_ix",
DISC_METADATA_REMOVE_KEY,
),
(
"spl_token_metadata_interface:update_the_authority",
DISC_METADATA_UPDATE_AUTHORITY,
),
("spl_token_metadata_interface:emitter", DISC_METADATA_EMIT),
(
"spl_token_group_interface:initialize_token_group",
DISC_GROUP_INITIALIZE,
),
(
"spl_token_group_interface:update_group_max_size",
DISC_GROUP_UPDATE_MAX_SIZE,
),
(
"spl_token_group_interface:update_authority",
DISC_GROUP_UPDATE_AUTHORITY,
),
(
"spl_token_group_interface:initialize_member",
DISC_GROUP_INITIALIZE_MEMBER,
),
] {
let digest = hopper_native::hash::sha256(&[input.as_bytes()]).unwrap();
assert_eq!(&digest[..8], &expected, "{input}");
}
}
#[test]
fn strings_are_length_prefixed_and_bounded() {
let data = encode_initialize("Hopper", "HOP", "https://hopperzero.dev/t.json").unwrap();
let bytes = data.as_slice();
assert_eq!(&bytes[..8], &DISC_METADATA_INITIALIZE);
assert_eq!(&bytes[8..12], &6u32.to_le_bytes());
assert_eq!(&bytes[12..18], b"Hopper");
assert_eq!(&bytes[18..22], &3u32.to_le_bytes());
assert_eq!(&bytes[22..25], b"HOP");
assert_eq!(bytes.len(), 8 + 4 + 6 + 4 + 3 + 4 + 29);
let long = "x".repeat(MAX_METADATA_INSTRUCTION_DATA);
assert_eq!(
encode_initialize(&long, "", "").err(),
Some(ProgramError::InvalidArgument)
);
let fits = "x".repeat(MAX_METADATA_INSTRUCTION_DATA - 8 - 12);
assert_eq!(
encode_initialize(&fits, "", "").unwrap().as_slice().len(),
MAX_METADATA_INSTRUCTION_DATA
);
}
#[test]
fn fields_options_and_authorities_encode_as_borsh() {
assert_eq!(
&encode_update_field(MetadataField::Uri, "u")
.unwrap()
.as_slice()[8..],
&[2, 1, 0, 0, 0, b'u']
);
assert_eq!(
&encode_update_field(MetadataField::Key("k"), "v")
.unwrap()
.as_slice()[8..],
&[3, 1, 0, 0, 0, b'k', 1, 0, 0, 0, b'v']
);
assert_eq!(
&encode_remove_key(true, "k").unwrap().as_slice()[8..],
&[1, 1, 0, 0, 0, b'k']
);
assert_eq!(&encode_emit(None, None).unwrap().as_slice()[8..], &[0, 0]);
assert_eq!(
&encode_emit(Some(2), None).unwrap().as_slice()[8..],
&[1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
);
let authority = Address::new_from_array([7; 32]);
assert_eq!(
&encode_update_authority(&DISC_GROUP_UPDATE_AUTHORITY, Some(&authority))
.unwrap()
.as_slice()[8..],
&[7u8; 32]
);
assert_eq!(
&encode_update_authority(&DISC_METADATA_UPDATE_AUTHORITY, None)
.unwrap()
.as_slice()[8..],
&[0u8; 32]
);
assert!(encode_update_authority(
&DISC_METADATA_UPDATE_AUTHORITY,
Some(&Address::new_from_array([0; 32]))
)
.is_err());
assert_eq!(
&encode_initialize_group(None, 9).unwrap().as_slice()[40..],
&9u64.to_le_bytes()
);
assert_eq!(encode_initialize_member().as_slice().len(), 8);
}
}