Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token 22: Add functionality to update extra_account_metas after initializing. #5894

Merged
merged 40 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e6bba4a
Added update function for ExtraAccountMetaList
tonton-sol Nov 26, 2023
fda5712
Updated interface to handle new update instruction
tonton-sol Nov 26, 2023
cf13287
Updated Cli to handle update command
tonton-sol Nov 26, 2023
84d8ac0
updated example program to handle updating
tonton-sol Nov 26, 2023
ca29fdc
Rust fmt trailing whitespace fix
tonton-sol Nov 27, 2023
2882a06
Removed unused variable
tonton-sol Nov 27, 2023
986e639
Added more explicit update instruction doc comment
tonton-sol Nov 27, 2023
3b177ce
Allow for resizing to smaller account size
tonton-sol Nov 28, 2023
5dc4a1a
Removed system program from update instruction
tonton-sol Nov 28, 2023
12c84cd
Added helper fn to calculate transfer lamports
tonton-sol Nov 28, 2023
155319d
Added unit tests for update function
tonton-sol Nov 28, 2023
2f3e041
Added unit test for update instruction
tonton-sol Nov 28, 2023
8b477d5
removed unnecessary commented out code
tonton-sol Nov 28, 2023
3a438b7
re-added checks on initialization
tonton-sol Nov 28, 2023
6334120
turned of zero_init for realloc for performance
tonton-sol Nov 28, 2023
b34c50c
Fixed update doc comments
tonton-sol Nov 29, 2023
2003517
Used block-scoping rather than explicit drop()
tonton-sol Nov 29, 2023
a69cfef
Removed unnecessary convert to vec
tonton-sol Nov 29, 2023
32d638b
refactored updated test into single test
tonton-sol Nov 29, 2023
93dfae6
added additional off-chain test of update instruct
tonton-sol Nov 29, 2023
666a36b
made on-chain invoke update test to match original
tonton-sol Nov 29, 2023
4262199
moved helper function up to others
tonton-sol Nov 29, 2023
54e77be
refactored create and update with helpers
tonton-sol Nov 29, 2023
de0e2f8
rustfmt: fix
tonton-sol Nov 30, 2023
f5b1a36
rustfmt: fix
tonton-sol Nov 30, 2023
f20c007
removed commented out system program in update
tonton-sol Nov 30, 2023
5e62a04
renamed helpers and removed unnecessary helper
tonton-sol Nov 30, 2023
1ba6c59
moved test helper up
tonton-sol Dec 1, 2023
9a22ce8
fixed test attribute location
tonton-sol Dec 1, 2023
9cf2129
removed multiple init extra account metas in test
tonton-sol Dec 1, 2023
4350929
added instruction assert to update test
tonton-sol Dec 1, 2023
a3e6c7e
renamed transfer address to extra account metas
tonton-sol Dec 2, 2023
58d7011
rustfmt: comment fix
tonton-sol Dec 3, 2023
451aaf4
clippy: fix
tonton-sol Dec 3, 2023
9fe5c86
added update test with simple PDA
tonton-sol Dec 3, 2023
a944952
made more changes to updated metas in test
tonton-sol Dec 3, 2023
ce76bf9
added check for if extra metas have be initialized
tonton-sol Dec 3, 2023
b8299b3
Merge branch 'master' into master
tonton-sol Dec 5, 2023
20d9185
spelling fix
tonton-sol Dec 7, 2023
9a508de
fixed initialized condition
tonton-sol Dec 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions libraries/tlv-account-resolution/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ impl ExtraAccountMetaList {
Ok(())
}

/// Update pod slice data for the given instruction and its required
/// list of `ExtraAccountMeta`s
pub fn update<T: SplDiscriminate>(
data: &mut [u8],
extra_account_metas: &[ExtraAccountMeta],
) -> Result<(), ProgramError> {
let mut state = TlvStateMut::unpack(data).unwrap();
let tlv_size = PodSlice::<ExtraAccountMeta>::size_of(extra_account_metas.len())?;
let bytes = state.realloc_first::<T>(tlv_size)?;
let mut validation_data = PodSliceMut::init(bytes)?;
for meta in extra_account_metas {
validation_data.push(*meta)?;
}
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}

/// Get the underlying `PodSlice<ExtraAccountMeta>` from an unpacked TLV
///
/// Due to lifetime annoyances, this function can't just take in the bytes,
Expand Down
150 changes: 149 additions & 1 deletion token/transfer-hook/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use {
},
spl_tlv_account_resolution::state::ExtraAccountMetaList,
spl_transfer_hook_interface::{
get_extra_account_metas_address, instruction::initialize_extra_account_meta_list,
get_extra_account_metas_address,
instruction::{initialize_extra_account_meta_list, update_extra_account_meta_list},
},
std::{fmt, process::exit, rc::Rc, str::FromStr},
strum_macros::{EnumString, IntoStaticStr},
Expand Down Expand Up @@ -127,6 +128,71 @@ async fn process_create_extra_account_metas(
.map_err(|err| format!("error: send transaction: {err}").into())
}

async fn process_update_extra_account_metas(
rpc_client: &RpcClient,
program_id: &Pubkey,
token: &Pubkey,
transfer_hook_accounts: Vec<AccountMeta>,
mint_authority: &dyn Signer,
payer: &dyn Signer,
) -> Result<Signature, Box<dyn std::error::Error>> {
let extra_account_metas_address = get_extra_account_metas_address(token, program_id);
let extra_account_metas = transfer_hook_accounts
.into_iter()
.map(|v| v.into())
.collect::<Vec<_>>();

let length = extra_account_metas.len();
let account_size = ExtraAccountMetaList::size_of(length)?;
let required_lamports = rpc_client
.get_minimum_balance_for_rent_exemption(account_size)
.await
.map_err(|err| format!("error: unable to fetch rent-exemption: {err}"))?;
let extra_account_metas_account = rpc_client.get_account(&extra_account_metas_address).await;
if extra_account_metas_account.is_err() {
return Err(format!(
"error: extra account metas for mint {token} and program {program_id} does not exist"
)
.into());
}
let current_lamports = extra_account_metas_account.map(|a| a.lamports).unwrap_or(0);
let transfer_lamports = required_lamports.saturating_sub(current_lamports);
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved

let mut ixs = vec![];
if transfer_lamports > 0 {
ixs.push(system_instruction::transfer(
&payer.pubkey(),
&extra_account_metas_address,
transfer_lamports,
));
}
ixs.push(update_extra_account_meta_list(
program_id,
&extra_account_metas_address,
token,
&mint_authority.pubkey(),
&extra_account_metas,
));

let mut transaction = Transaction::new_with_payer(&ixs, Some(&payer.pubkey()));
let blockhash = rpc_client
.get_latest_blockhash()
.await
.map_err(|err| format!("error: unable to get latest blockhash: {err}"))?;
let mut signers = vec![payer];
if payer.pubkey() != mint_authority.pubkey() {
signers.push(mint_authority);
}
transaction
.try_sign(&signers, blockhash)
.map_err(|err| format!("error: failed to sign transaction: {err}"))?;

rpc_client
.send_and_confirm_transaction_with_spinner(&transaction)
.await
.map_err(|err| format!("error: send transaction: {err}").into())
}
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_matches = Command::new(crate_name!())
Expand Down Expand Up @@ -217,6 +283,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.global(true)
.help("Filepath or URL to mint-authority keypair [default: client keypair]"),
)
)
.subcommand(
Command::new("update-extra-metas")
.about("Update the extra account metas account for a transfer hook program")
.arg(
Arg::with_name("program_id")
.validator(clap_is_valid_pubkey)
.value_name("TRANSFER_HOOK_PROGRAM")
.takes_value(true)
.index(1)
.required(true)
.help("The transfer hook program id"),
)
.arg(
Arg::with_name("token")
.validator(clap_is_valid_pubkey)
.value_name("TOKEN_MINT_ADDRESS")
.takes_value(true)
.index(2)
.required(true)
.help("The token mint address for the transfer hook"),
)
.arg(
Arg::with_name("transfer_hook_account")
.value_parser(parse_transfer_hook_account)
.value_name("PUBKEY:ROLE")
.takes_value(true)
.multiple(true)
.min_values(0)
.index(3)
.help("Additional pubkey(s) required for a transfer hook and their \
role, in the format \"<PUBKEY>:<ROLE>\". The role must be \
\"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".")
)
.arg(
Arg::new("mint_authority")
.long("mint-authority")
.value_name("KEYPAIR")
.validator(|s| is_valid_signer(s))
.takes_value(true)
.global(true)
.help("Filepath or URL to mint-authority keypair [default: client keypair]"),
)
).get_matches();

let (command, matches) = app_matches.subcommand().unwrap();
Expand Down Expand Up @@ -303,6 +412,45 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
println!("Signature: {signature}");
}
("update-extra-metas", arg_matches) => {
let program_id = pubkey_of_signer(arg_matches, "program_id", &mut wallet_manager)
.unwrap()
.unwrap();
let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager)
.unwrap()
.unwrap();
let transfer_hook_accounts = arg_matches
.get_many::<AccountMeta>("transfer_hook_account")
.unwrap_or_default()
.cloned()
.collect();
let mint_authority = DefaultSigner::new(
"mint_authority",
matches
.value_of("mint_authority")
.map(|s| s.to_string())
.unwrap_or_else(|| cli_config.keypair_path.clone()),
)
.signer_from_path(matches, &mut wallet_manager)
.unwrap_or_else(|err| {
eprintln!("error: {err}");
exit(1);
});
let signature = process_update_extra_account_metas(
&rpc_client,
&program_id,
&token,
transfer_hook_accounts,
mint_authority.as_ref(),
config.default_signer.as_ref(),
)
.await
.unwrap_or_else(|err| {
eprintln!("error: send transaction: {err}");
exit(1);
});
println!("Signature: {signature}");
}
_ => unreachable!(),
};

Expand Down
59 changes: 59 additions & 0 deletions token/transfer-hook/example/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,59 @@ pub fn process_initialize_extra_account_meta_list(
Ok(())
}

/// Processes a
/// [UpdateExtraAccountMetaList](enum.TransferHookInstruction.html)
/// instruction.
pub fn process_update_extra_account_meta_list(
program_id: &Pubkey,
accounts: &[AccountInfo],
extra_account_metas: &[ExtraAccountMeta],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();

let extra_account_metas_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let _system_program_info = next_account_info(account_info_iter)?;

// check that the mint authority is valid without fully deserializing
let mint_data = mint_info.try_borrow_data()?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
let mint_authority = mint
.base
.mint_authority
.ok_or(TransferHookError::MintHasNoMintAuthority)?;

// Check signers
if !authority_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if *authority_info.key != mint_authority {
return Err(TransferHookError::IncorrectMintAuthority.into());
}

// Check validation account
let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id);
if expected_validation_address != *extra_account_metas_info.key {
return Err(ProgramError::InvalidSeeds);
}

let length = extra_account_metas.len();
let account_size = ExtraAccountMetaList::size_of(length)?;
let original_account_size = extra_account_metas_info.data_len();

// If the new extra_account_metas is larger resize the account
if account_size > original_account_size {
extra_account_metas_info.realloc(account_size, true)?;
}
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved

// Update the data
let mut data = extra_account_metas_info.try_borrow_mut_data()?;
ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, extra_account_metas)?;

Ok(())
}

/// Processes an [Instruction](enum.Instruction.html).
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let instruction = TransferHookInstruction::unpack(input)?;
Expand All @@ -149,5 +202,11 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P
msg!("Instruction: InitializeExtraAccountMetaList");
process_initialize_extra_account_meta_list(program_id, accounts, &extra_account_metas)
}
TransferHookInstruction::UpdateExtraAccountMetaList {
extra_account_metas,
} => {
msg!("Instruction: UpdateExtraAccountMetaList");
process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas)
}
}
}
62 changes: 62 additions & 0 deletions token/transfer-hook/interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ pub enum TransferHookInstruction {
/// List of `ExtraAccountMeta`s to write into the account
extra_account_metas: Vec<ExtraAccountMeta>,
},
/// Updates the extra account metas on an account, writing into
/// the first open TLV space.
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved
///
/// Accounts expected by this instruction:
///
/// 0. `[w]` Account with extra account metas
/// 1. `[]` Mint
/// 2. `[s]` Mint authority
/// 3. `[]` System program
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved
UpdateExtraAccountMetaList {
/// List of `ExtraAccountMeta`s to write into the account
tonton-sol marked this conversation as resolved.
Show resolved Hide resolved
extra_account_metas: Vec<ExtraAccountMeta>,
},
}
/// TLV instruction type only used to define the discriminator. The actual data
/// is entirely managed by `ExtraAccountMetaList`, and it is the only data
Expand All @@ -59,6 +72,12 @@ pub struct ExecuteInstruction;
#[discriminator_hash_input("spl-transfer-hook-interface:initialize-extra-account-metas")]
pub struct InitializeExtraAccountMetaListInstruction;

/// TLV instruction type used to update extra account metas
/// for the transfer hook
#[derive(SplDiscriminate)]
#[discriminator_hash_input("spl-transfer-hook-interface:update-extra-account-metas")]
pub struct UpdateExtraAccountMetaListInstruction;

impl TransferHookInstruction {
/// Unpacks a byte buffer into a
/// [TransferHookInstruction](enum.TransferHookInstruction.html).
Expand All @@ -83,6 +102,13 @@ impl TransferHookInstruction {
extra_account_metas,
}
}
UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => {
let pod_slice = PodSlice::<ExtraAccountMeta>::unpack(rest)?;
let extra_account_metas = pod_slice.data().to_vec();
Self::UpdateExtraAccountMetaList {
extra_account_metas,
}
}
_ => return Err(ProgramError::InvalidInstructionData),
})
}
Expand All @@ -105,6 +131,15 @@ impl TransferHookInstruction {
buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
}
Self::UpdateExtraAccountMetaList {
extra_account_metas,
} => {
buf.extend_from_slice(
UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE,
);
buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes());
buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas));
}
};
buf
}
Expand Down Expand Up @@ -189,6 +224,33 @@ pub fn initialize_extra_account_meta_list(
}
}

/// Creates a `UpdateExtraAccountMetaList` instruction.
pub fn update_extra_account_meta_list(
program_id: &Pubkey,
extra_account_metas_pubkey: &Pubkey,
mint_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
extra_account_metas: &[ExtraAccountMeta],
) -> Instruction {
let data = TransferHookInstruction::UpdateExtraAccountMetaList {
extra_account_metas: extra_account_metas.to_vec(),
}
.pack();

let accounts = vec![
AccountMeta::new(*extra_account_metas_pubkey, false),
AccountMeta::new_readonly(*mint_pubkey, false),
AccountMeta::new_readonly(*authority_pubkey, true),
AccountMeta::new_readonly(system_program::id(), false),
];

Instruction {
program_id: *program_id,
accounts,
data,
}
}

#[cfg(test)]
mod test {
use {super::*, crate::NAMESPACE, solana_program::hash, spl_pod::bytemuck::pod_from_bytes};
Expand Down