diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs deleted file mode 100644 index e5c87d7f5..000000000 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ /dev/null @@ -1,546 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::{token_2022::spl_token_2022::instruction::AuthorityType, token_interface}; -use ntt_messages::chain_id::ChainId; -use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable}; - -#[cfg(feature = "idl-build")] -use crate::messages::Hack; - -use crate::{ - config::Config, - error::NTTError, - peer::NttManagerPeer, - pending_token_authority::PendingTokenAuthority, - queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, - registered_transceiver::RegisteredTransceiver, -}; - -// * Transfer ownership - -/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the -/// new owner, and the second step is for the new owner to claim the ownership. -/// This is to prevent a situation where the ownership is transferred to an -/// address that is not able to claim the ownership (by mistake). -/// -/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`] -/// instruction. -/// -/// Alternatively, the ownership can be transferred in a single step by calling the -/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner -/// cannot actually sign transactions (due to setting the wrong address), the program will be -/// permanently locked. If the intention is to transfer ownership to a program using this instruction, -/// take extra care to ensure that the owner is a PDA, not the program address itself. -#[derive(Accounts)] -pub struct TransferOwnership<'info> { - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - /// CHECK: This account will be the signer in the [claim_ownership] instruction. - new_owner: UncheckedAccount<'info>, - - #[account( - seeds = [b"upgrade_lock"], - bump, - )] - /// CHECK: The seeds constraint enforces that this is the correct address - upgrade_lock: UncheckedAccount<'info>, - - #[account( - mut, - seeds = [crate::ID.as_ref()], - bump, - seeds::program = bpf_loader_upgradeable_program, - )] - program_data: Account<'info, ProgramData>, - - bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, -} - -pub fn transfer_ownership(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = Some(ctx.accounts.new_owner.key()); - - // TODO: only transfer authority when the authority is not already the upgrade lock - bpf_loader_upgradeable::set_upgrade_authority_checked( - CpiContext::new_with_signer( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthorityChecked { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.owner.to_account_info(), - new_authority: ctx.accounts.upgrade_lock.to_account_info(), - }, - &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], - ), - &crate::ID, - ) -} - -pub fn transfer_ownership_one_step_unchecked(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = None; - ctx.accounts.config.owner = ctx.accounts.new_owner.key(); - - // NOTE: unlike in `transfer_ownership`, we use the unchecked version of the - // `set_upgrade_authority` instruction here. The checked version requires - // the new owner to be a signer, which is what we want to avoid here. - bpf_loader_upgradeable::set_upgrade_authority( - CpiContext::new( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthority { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.owner.to_account_info(), - new_authority: Some(ctx.accounts.new_owner.to_account_info()), - }, - ), - &crate::ID, - ) -} - -// * Claim ownership - -#[derive(Accounts)] -pub struct ClaimOwnership<'info> { - #[account( - mut, - constraint = ( - config.pending_owner == Some(new_owner.key()) - || config.owner == new_owner.key() - ) @ NTTError::InvalidPendingOwner - )] - pub config: Account<'info, Config>, - - #[account( - seeds = [b"upgrade_lock"], - bump, - )] - /// CHECK: The seeds constraint enforces that this is the correct address - upgrade_lock: UncheckedAccount<'info>, - - pub new_owner: Signer<'info>, - - #[account( - mut, - seeds = [crate::ID.as_ref()], - bump, - seeds::program = bpf_loader_upgradeable_program, - )] - program_data: Account<'info, ProgramData>, - - bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, -} - -pub fn claim_ownership(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = None; - ctx.accounts.config.owner = ctx.accounts.new_owner.key(); - - bpf_loader_upgradeable::set_upgrade_authority_checked( - CpiContext::new_with_signer( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthorityChecked { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.upgrade_lock.to_account_info(), - new_authority: ctx.accounts.new_owner.to_account_info(), - }, - &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], - ), - &crate::ID, - ) -} - -// * Set token authority - -#[derive(Accounts)] -pub struct AcceptTokenAuthority<'info> { - #[account( - has_one = mint, - constraint = config.paused @ NTTError::NotPaused, - )] - pub config: Account<'info, Config>, - - #[account(mut)] - pub mint: InterfaceAccount<'info, token_interface::Mint>, - - #[account( - seeds = [crate::TOKEN_AUTHORITY_SEED], - bump, - )] - /// CHECK: The constraints enforce this is valid mint authority - pub token_authority: UncheckedAccount<'info>, - - pub current_authority: Signer<'info>, - - pub token_program: Interface<'info, token_interface::TokenInterface>, -} - -pub fn accept_token_authority(ctx: Context) -> Result<()> { - token_interface::set_authority( - CpiContext::new( - ctx.accounts.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.mint.to_account_info(), - current_authority: ctx.accounts.current_authority.to_account_info(), - }, - ), - AuthorityType::MintTokens, - Some(ctx.accounts.token_authority.key()), - ) -} - -#[derive(Accounts)] -pub struct SetTokenAuthority<'info> { - #[account( - has_one = owner, - has_one = mint, - constraint = config.paused @ NTTError::NotPaused, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account(mut)] - pub mint: InterfaceAccount<'info, token_interface::Mint>, - - #[account( - seeds = [crate::TOKEN_AUTHORITY_SEED], - bump, - )] - /// CHECK: The constraints enforce this is valid mint authority - pub token_authority: UncheckedAccount<'info>, - - /// CHECK: This account will be the signer in the [claim_token_authority] instruction. - pub new_authority: UncheckedAccount<'info>, -} - -#[derive(Accounts)] -pub struct SetTokenAuthorityChecked<'info> { - #[account( - constraint = common.token_authority.key() == common.mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority - )] - pub common: SetTokenAuthority<'info>, - - #[account(mut)] - pub rent_payer: Signer<'info>, - - #[account( - init_if_needed, - space = 8 + PendingTokenAuthority::INIT_SPACE, - payer = rent_payer, - seeds = [PendingTokenAuthority::SEED_PREFIX], - bump - )] - pub pending_token_authority: Account<'info, PendingTokenAuthority>, - - pub system_program: Program<'info, System>, -} - -pub fn set_token_authority(ctx: Context) -> Result<()> { - ctx.accounts - .pending_token_authority - .set_inner(PendingTokenAuthority { - bump: ctx.bumps.pending_token_authority, - pending_authority: ctx.accounts.common.new_authority.key(), - rent_payer: ctx.accounts.rent_payer.key(), - }); - Ok(()) -} - -#[derive(Accounts)] -pub struct SetTokenAuthorityUnchecked<'info> { - pub common: SetTokenAuthority<'info>, - - pub token_program: Interface<'info, token_interface::TokenInterface>, -} - -pub fn set_token_authority_one_step_unchecked( - ctx: Context, -) -> Result<()> { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.common.new_authority.key()), - ) -} - -// * Claim token authority - -#[derive(Accounts)] -pub struct ClaimTokenAuthorityBase<'info> { - #[account( - has_one = mint, - constraint = config.paused @ NTTError::NotPaused, - )] - pub config: Account<'info, Config>, - - #[account(mut)] - pub mint: InterfaceAccount<'info, token_interface::Mint>, - - #[account( - seeds = [crate::TOKEN_AUTHORITY_SEED], - bump, - )] - /// CHECK: The seeds constraint enforces that this is the correct address - pub token_authority: UncheckedAccount<'info>, - - #[account(mut)] - /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address - pub rent_payer: UncheckedAccount<'info>, - - #[account( - mut, - seeds = [PendingTokenAuthority::SEED_PREFIX], - bump = pending_token_authority.bump, - has_one = rent_payer @ NTTError::IncorrectRentPayer, - close = rent_payer - )] - pub pending_token_authority: Account<'info, PendingTokenAuthority>, - - pub token_program: Interface<'info, token_interface::TokenInterface>, - - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct RevertTokenAuthority<'info> { - pub common: ClaimTokenAuthorityBase<'info>, - - #[account( - address = common.config.owner - )] - pub owner: Signer<'info>, -} - -pub fn revert_token_authority(_ctx: Context) -> Result<()> { - Ok(()) -} - -#[derive(Accounts)] -pub struct ClaimTokenAuthority<'info> { - pub common: ClaimTokenAuthorityBase<'info>, - - #[account( - address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority - )] - pub new_authority: Signer<'info>, -} - -pub fn claim_token_authority(ctx: Context) -> Result<()> { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_authority.key()), - ) -} - -// * Set peers - -#[derive(Accounts)] -#[instruction(args: SetPeerArgs)] -pub struct SetPeer<'info> { - #[account(mut)] - pub payer: Signer<'info>, - - pub owner: Signer<'info>, - - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - #[account( - init_if_needed, - space = 8 + NttManagerPeer::INIT_SPACE, - payer = payer, - seeds = [NttManagerPeer::SEED_PREFIX, args.chain_id.id.to_be_bytes().as_ref()], - bump - )] - pub peer: Account<'info, NttManagerPeer>, - - #[account( - init_if_needed, - space = 8 + InboxRateLimit::INIT_SPACE, - payer = payer, - seeds = [ - InboxRateLimit::SEED_PREFIX, - args.chain_id.id.to_be_bytes().as_ref() - ], - bump, - )] - pub inbox_rate_limit: Account<'info, InboxRateLimit>, - - pub system_program: Program<'info, System>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetPeerArgs { - pub chain_id: ChainId, - pub address: [u8; 32], - pub limit: u64, - /// The token decimals on the peer chain. - pub token_decimals: u8, -} - -pub fn set_peer(ctx: Context, args: SetPeerArgs) -> Result<()> { - ctx.accounts.peer.set_inner(NttManagerPeer { - bump: ctx.bumps.peer, - address: args.address, - token_decimals: args.token_decimals, - }); - - ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit { - bump: ctx.bumps.inbox_rate_limit, - rate_limit: RateLimitState::new(args.limit), - }); - Ok(()) -} - -// * Register transceivers - -#[derive(Accounts)] -pub struct RegisterTransceiver<'info> { - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account(mut)] - pub payer: Signer<'info>, - - #[account(executable)] - /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be - /// used here that wraps the Transceiver account type. - pub transceiver: UncheckedAccount<'info>, - - #[account( - init, - space = 8 + RegisteredTransceiver::INIT_SPACE, - payer = payer, - seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], - bump - )] - pub registered_transceiver: Account<'info, RegisteredTransceiver>, - - pub system_program: Program<'info, System>, -} - -pub fn register_transceiver(ctx: Context) -> Result<()> { - let id = ctx.accounts.config.next_transceiver_id; - ctx.accounts.config.next_transceiver_id += 1; - ctx.accounts - .registered_transceiver - .set_inner(RegisteredTransceiver { - bump: ctx.bumps.registered_transceiver, - id, - transceiver_address: ctx.accounts.transceiver.key(), - }); - - ctx.accounts.config.enabled_transceivers.set(id, true)?; - Ok(()) -} - -// * Limit rate adjustment -#[derive(Accounts)] -pub struct SetOutboundLimit<'info> { - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account(mut)] - pub rate_limit: Account<'info, OutboxRateLimit>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetOutboundLimitArgs { - pub limit: u64, -} - -pub fn set_outbound_limit( - ctx: Context, - args: SetOutboundLimitArgs, -) -> Result<()> { - ctx.accounts.rate_limit.set_limit(args.limit); - Ok(()) -} - -#[derive(Accounts)] -#[instruction(args: SetInboundLimitArgs)] -pub struct SetInboundLimit<'info> { - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account( - mut, - seeds = [ - InboxRateLimit::SEED_PREFIX, - args.chain_id.id.to_be_bytes().as_ref() - ], - bump = rate_limit.bump - )] - pub rate_limit: Account<'info, InboxRateLimit>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetInboundLimitArgs { - pub limit: u64, - pub chain_id: ChainId, -} - -pub fn set_inbound_limit(ctx: Context, args: SetInboundLimitArgs) -> Result<()> { - ctx.accounts.rate_limit.set_limit(args.limit); - Ok(()) -} - -// * Pausing -#[derive(Accounts)] -pub struct SetPaused<'info> { - pub owner: Signer<'info>, - - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, -} - -pub fn set_paused(ctx: Context, paused: bool) -> Result<()> { - ctx.accounts.config.paused = paused; - Ok(()) -} diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs new file mode 100644 index 000000000..a8cb12c5e --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs @@ -0,0 +1,202 @@ +use anchor_lang::prelude::*; +use ntt_messages::chain_id::ChainId; + +use crate::{ + config::Config, + peer::NttManagerPeer, + queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, + registered_transceiver::RegisteredTransceiver, +}; + +pub mod transfer_ownership; +pub mod transfer_token_authority; + +pub use transfer_ownership::*; +pub use transfer_token_authority::*; + +// * Set peers + +#[derive(Accounts)] +#[instruction(args: SetPeerArgs)] +pub struct SetPeer<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + pub owner: Signer<'info>, + + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + #[account( + init_if_needed, + space = 8 + NttManagerPeer::INIT_SPACE, + payer = payer, + seeds = [NttManagerPeer::SEED_PREFIX, args.chain_id.id.to_be_bytes().as_ref()], + bump + )] + pub peer: Account<'info, NttManagerPeer>, + + #[account( + init_if_needed, + space = 8 + InboxRateLimit::INIT_SPACE, + payer = payer, + seeds = [ + InboxRateLimit::SEED_PREFIX, + args.chain_id.id.to_be_bytes().as_ref() + ], + bump, + )] + pub inbox_rate_limit: Account<'info, InboxRateLimit>, + + pub system_program: Program<'info, System>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetPeerArgs { + pub chain_id: ChainId, + pub address: [u8; 32], + pub limit: u64, + /// The token decimals on the peer chain. + pub token_decimals: u8, +} + +pub fn set_peer(ctx: Context, args: SetPeerArgs) -> Result<()> { + ctx.accounts.peer.set_inner(NttManagerPeer { + bump: ctx.bumps.peer, + address: args.address, + token_decimals: args.token_decimals, + }); + + ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit { + bump: ctx.bumps.inbox_rate_limit, + rate_limit: RateLimitState::new(args.limit), + }); + Ok(()) +} + +// * Register transceivers + +#[derive(Accounts)] +pub struct RegisterTransceiver<'info> { + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(executable)] + /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be + /// used here that wraps the Transceiver account type. + pub transceiver: UncheckedAccount<'info>, + + #[account( + init, + space = 8 + RegisteredTransceiver::INIT_SPACE, + payer = payer, + seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], + bump + )] + pub registered_transceiver: Account<'info, RegisteredTransceiver>, + + pub system_program: Program<'info, System>, +} + +pub fn register_transceiver(ctx: Context) -> Result<()> { + let id = ctx.accounts.config.next_transceiver_id; + ctx.accounts.config.next_transceiver_id += 1; + ctx.accounts + .registered_transceiver + .set_inner(RegisteredTransceiver { + bump: ctx.bumps.registered_transceiver, + id, + transceiver_address: ctx.accounts.transceiver.key(), + }); + + ctx.accounts.config.enabled_transceivers.set(id, true)?; + Ok(()) +} + +// * Limit rate adjustment + +#[derive(Accounts)] +pub struct SetOutboundLimit<'info> { + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub rate_limit: Account<'info, OutboxRateLimit>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetOutboundLimitArgs { + pub limit: u64, +} + +pub fn set_outbound_limit( + ctx: Context, + args: SetOutboundLimitArgs, +) -> Result<()> { + ctx.accounts.rate_limit.set_limit(args.limit); + Ok(()) +} + +#[derive(Accounts)] +#[instruction(args: SetInboundLimitArgs)] +pub struct SetInboundLimit<'info> { + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account( + mut, + seeds = [ + InboxRateLimit::SEED_PREFIX, + args.chain_id.id.to_be_bytes().as_ref() + ], + bump = rate_limit.bump + )] + pub rate_limit: Account<'info, InboxRateLimit>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetInboundLimitArgs { + pub limit: u64, + pub chain_id: ChainId, +} + +pub fn set_inbound_limit(ctx: Context, args: SetInboundLimitArgs) -> Result<()> { + ctx.accounts.rate_limit.set_limit(args.limit); + Ok(()) +} + +// * Pausing + +#[derive(Accounts)] +pub struct SetPaused<'info> { + pub owner: Signer<'info>, + + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, +} + +pub fn set_paused(ctx: Context, paused: bool) -> Result<()> { + ctx.accounts.config.paused = paused; + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs new file mode 100644 index 000000000..e9ef8129e --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs @@ -0,0 +1,148 @@ +use anchor_lang::prelude::*; +use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable}; + +#[cfg(feature = "idl-build")] +use crate::messages::Hack; + +use crate::{config::Config, error::NTTError}; + +// * Transfer ownership + +/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the +/// new owner, and the second step is for the new owner to claim the ownership. +/// This is to prevent a situation where the ownership is transferred to an +/// address that is not able to claim the ownership (by mistake). +/// +/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`] +/// instruction. +/// +/// Alternatively, the ownership can be transferred in a single step by calling the +/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner +/// cannot actually sign transactions (due to setting the wrong address), the program will be +/// permanently locked. If the intention is to transfer ownership to a program using this instruction, +/// take extra care to ensure that the owner is a PDA, not the program address itself. +#[derive(Accounts)] +pub struct TransferOwnership<'info> { + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + /// CHECK: This account will be the signer in the [claim_ownership] instruction. + new_owner: UncheckedAccount<'info>, + + #[account( + seeds = [b"upgrade_lock"], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct address + upgrade_lock: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable_program, + )] + program_data: Account<'info, ProgramData>, + + bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, +} + +pub fn transfer_ownership(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = Some(ctx.accounts.new_owner.key()); + + // TODO: only transfer authority when the authority is not already the upgrade lock + bpf_loader_upgradeable::set_upgrade_authority_checked( + CpiContext::new_with_signer( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthorityChecked { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.owner.to_account_info(), + new_authority: ctx.accounts.upgrade_lock.to_account_info(), + }, + &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], + ), + &crate::ID, + ) +} + +pub fn transfer_ownership_one_step_unchecked(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = None; + ctx.accounts.config.owner = ctx.accounts.new_owner.key(); + + // NOTE: unlike in `transfer_ownership`, we use the unchecked version of the + // `set_upgrade_authority` instruction here. The checked version requires + // the new owner to be a signer, which is what we want to avoid here. + bpf_loader_upgradeable::set_upgrade_authority( + CpiContext::new( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthority { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.owner.to_account_info(), + new_authority: Some(ctx.accounts.new_owner.to_account_info()), + }, + ), + &crate::ID, + ) +} + +// * Claim ownership + +#[derive(Accounts)] +pub struct ClaimOwnership<'info> { + #[account( + mut, + constraint = ( + config.pending_owner == Some(new_owner.key()) + || config.owner == new_owner.key() + ) @ NTTError::InvalidPendingOwner + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [b"upgrade_lock"], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct address + upgrade_lock: UncheckedAccount<'info>, + + pub new_owner: Signer<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable_program, + )] + program_data: Account<'info, ProgramData>, + + bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, +} + +pub fn claim_ownership(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = None; + ctx.accounts.config.owner = ctx.accounts.new_owner.key(); + + bpf_loader_upgradeable::set_upgrade_authority_checked( + CpiContext::new_with_signer( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthorityChecked { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.upgrade_lock.to_account_info(), + new_authority: ctx.accounts.new_owner.to_account_info(), + }, + &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], + ), + &crate::ID, + ) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs new file mode 100644 index 000000000..c809331d5 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs @@ -0,0 +1,386 @@ +use anchor_lang::prelude::*; +use anchor_spl::{token_2022::spl_token_2022::instruction::AuthorityType, token_interface}; + +use crate::{ + config::Config, error::NTTError, pending_token_authority::PendingTokenAuthority, + spl_multisig::SplMultisig, +}; + +// * Accept token authority + +#[derive(Accounts)] +pub struct AcceptTokenAuthorityBase<'info> { + #[account( + has_one = mint, + constraint = config.paused @ NTTError::NotPaused, + )] + pub config: Account<'info, Config>, + + #[account(mut)] + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: The constraints enforce this is valid mint authority + pub token_authority: UncheckedAccount<'info>, + + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, +} + +#[derive(Accounts)] +pub struct AcceptTokenAuthority<'info> { + pub common: AcceptTokenAuthorityBase<'info>, + + pub current_authority: Signer<'info>, +} + +pub fn accept_token_authority(ctx: Context) -> Result<()> { + token_interface::set_authority( + CpiContext::new( + ctx.accounts.common.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.current_authority.to_account_info(), + }, + ), + AuthorityType::MintTokens, + Some(match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => multisig_token_authority.key(), + None => ctx.accounts.common.token_authority.key(), + }), + ) +} + +#[derive(Accounts)] +pub struct AcceptTokenAuthorityFromMultisig<'info> { + pub common: AcceptTokenAuthorityBase<'info>, + + /// CHECK: The remaining accounts are treated as required signers for the multisig + pub current_multisig_authority: InterfaceAccount<'info, SplMultisig>, +} + +pub fn accept_token_authority_from_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>, +) -> Result<()> { + let new_authority = match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => multisig_token_authority.to_account_info(), + None => ctx.accounts.common.token_authority.to_account_info(), + }; + + let mut signer_pubkeys: Vec<&Pubkey> = Vec::new(); + let mut account_infos = vec![ + ctx.accounts.common.mint.to_account_info(), + new_authority.clone(), + ctx.accounts.current_multisig_authority.to_account_info(), + ]; + + // pass ctx.remaining_accounts as required signers + { + signer_pubkeys.extend(ctx.remaining_accounts.iter().map(|x| x.key)); + account_infos.extend_from_slice(ctx.remaining_accounts); + } + + solana_program::program::invoke( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &ctx.accounts.current_multisig_authority.key(), + &signer_pubkeys, + )?, + account_infos.as_slice(), + )?; + Ok(()) +} + +// * Set token authority + +#[derive(Accounts)] +pub struct SetTokenAuthority<'info> { + #[account( + has_one = owner, + has_one = mint, + constraint = config.paused @ NTTError::NotPaused, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: The constraints enforce this is valid mint authority + pub token_authority: UncheckedAccount<'info>, + + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, + + /// CHECK: This account will be the signer in the [claim_token_authority] instruction. + pub new_authority: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct SetTokenAuthorityUnchecked<'info> { + pub common: SetTokenAuthority<'info>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, +} + +pub fn set_token_authority_one_step_unchecked( + ctx: Context, +) -> Result<()> { + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.common.new_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.common.new_authority.key(), + ), + } +} + +#[derive(Accounts)] +pub struct SetTokenAuthorityChecked<'info> { + #[account( + constraint = + common.mint.mint_authority.unwrap() == common.multisig_token_authority.as_ref().map_or( + common.token_authority.key(), + |multisig_token_authority| multisig_token_authority.key() + ) + @ NTTError::InvalidMintAuthority + )] + pub common: SetTokenAuthority<'info>, + + #[account(mut)] + pub rent_payer: Signer<'info>, + + #[account( + init_if_needed, + space = 8 + PendingTokenAuthority::INIT_SPACE, + payer = rent_payer, + seeds = [PendingTokenAuthority::SEED_PREFIX], + bump + )] + pub pending_token_authority: Account<'info, PendingTokenAuthority>, + + pub system_program: Program<'info, System>, +} + +pub fn set_token_authority(ctx: Context) -> Result<()> { + ctx.accounts + .pending_token_authority + .set_inner(PendingTokenAuthority { + bump: ctx.bumps.pending_token_authority, + pending_authority: ctx.accounts.common.new_authority.key(), + rent_payer: ctx.accounts.rent_payer.key(), + }); + Ok(()) +} + +// * Claim token authority + +#[derive(Accounts)] +pub struct ClaimTokenAuthorityBase<'info> { + #[account( + has_one = mint, + constraint = config.paused @ NTTError::NotPaused, + )] + pub config: Account<'info, Config>, + + #[account(mut)] + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct address + pub token_authority: UncheckedAccount<'info>, + + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, + + #[account(mut)] + /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address + pub rent_payer: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [PendingTokenAuthority::SEED_PREFIX], + bump = pending_token_authority.bump, + has_one = rent_payer @ NTTError::IncorrectRentPayer, + close = rent_payer + )] + pub pending_token_authority: Account<'info, PendingTokenAuthority>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct RevertTokenAuthority<'info> { + pub common: ClaimTokenAuthorityBase<'info>, + + #[account( + // there is no custom error thrown as this is usually checked via `has_one` on the config + address = common.config.owner + )] + pub owner: Signer<'info>, +} + +pub fn revert_token_authority(_ctx: Context) -> Result<()> { + Ok(()) +} + +#[derive(Accounts)] +pub struct ClaimTokenAuthority<'info> { + pub common: ClaimTokenAuthorityBase<'info>, + + #[account( + address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority + )] + pub new_authority: Signer<'info>, +} + +pub fn claim_token_authority(ctx: Context) -> Result<()> { + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_authority.key(), + ), + } +} + +#[derive(Accounts)] +pub struct ClaimTokenAuthorityToMultisig<'info> { + pub common: ClaimTokenAuthorityBase<'info>, + + #[account( + address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority + )] + /// CHECK: The remaining accounts are treated as required signers for the multisig to be validated + pub new_multisig_authority: InterfaceAccount<'info, SplMultisig>, +} + +pub fn claim_token_authority_to_multisig( + ctx: Context, +) -> Result<()> { + // SPL Multisig cannot be a Signer so we simulate multisig signing using ctx.remaining_accounts as + // required signers to validate it + { + let multisig = ctx.accounts.new_multisig_authority.to_account_info(); + token_interface::spl_token_2022::processor::Processor::validate_owner( + &ctx.accounts.common.token_program.key(), + &multisig.key(), + &multisig, + multisig.data_len(), + ctx.remaining_accounts, + )?; + } + + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_multisig_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_multisig_authority.key(), + ), + } +} + +fn claim_from_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_bump: u8, + new_authority: Pubkey, +) -> Result<()> { + token_interface::set_authority( + CpiContext::new_with_signer( + token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: mint.to_account_info(), + current_authority: token_authority.to_account_info(), + }, + &[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]], + ), + AuthorityType::MintTokens, + Some(new_authority), + )?; + Ok(()) +} + +fn claim_from_multisig_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + multisig_token_authority: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_bump: u8, + new_authority: Pubkey, +) -> Result<()> { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &token_program.key(), + &mint.key(), + Some(&new_authority), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&token_authority.key()], + )?, + &[mint, multisig_token_authority, token_authority], + &[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]], + )?; + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 7b480b498..f3aed6dfb 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -146,8 +146,10 @@ pub mod example_native_token_transfers { instructions::accept_token_authority(ctx) } - pub fn set_token_authority(ctx: Context) -> Result<()> { - instructions::set_token_authority(ctx) + pub fn accept_token_authority_from_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>, + ) -> Result<()> { + instructions::accept_token_authority_from_multisig(ctx) } pub fn set_token_authority_one_step_unchecked( @@ -156,6 +158,10 @@ pub mod example_native_token_transfers { instructions::set_token_authority_one_step_unchecked(ctx) } + pub fn set_token_authority(ctx: Context) -> Result<()> { + instructions::set_token_authority(ctx) + } + pub fn revert_token_authority(ctx: Context) -> Result<()> { instructions::revert_token_authority(ctx) } @@ -164,6 +170,12 @@ pub mod example_native_token_transfers { instructions::claim_token_authority(ctx) } + pub fn claim_token_authority_to_multisig( + ctx: Context, + ) -> Result<()> { + instructions::claim_token_authority_to_multisig(ctx) + } + pub fn set_paused(ctx: Context, pause: bool) -> Result<()> { instructions::set_paused(ctx, pause) } diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index a582890b5..00004c973 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -13,7 +13,6 @@ import { encoding, serialize, serializePayload, - signSendWait as ssw, } from "@wormhole-foundation/sdk"; import * as testing from "@wormhole-foundation/sdk-definitions/testing"; import { @@ -22,15 +21,21 @@ import { getSolanaSignAndSendSigner, } from "@wormhole-foundation/sdk-solana"; import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; -import * as fs from "fs"; -import { DummyTransferHook } from "../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; -import { getTransceiverProgram, IdlVersion, NTT } from "../ts/index.js"; -import { derivePda } from "../ts/lib/utils.js"; +import { IdlVersion, NTT, getTransceiverProgram } from "../ts/index.js"; import { SolanaNtt } from "../ts/sdk/index.js"; - -const solanaRootDir = `${__dirname}/../`; - +import { + TestDummyTransferHook, + TestHelper, + TestMint, + assert, + signSendWait, +} from "./utils/helpers.js"; + +/** + * Test Config Constants + */ +const SOLANA_ROOT_DIR = `${__dirname}/../`; const VERSION: IdlVersion = "3.0.0"; const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID; const GUARDIAN_KEY = @@ -41,84 +46,52 @@ const NTT_ADDRESS: anchor.web3.PublicKey = const WH_TRANSCEIVER_ADDRESS: anchor.web3.PublicKey = anchor.workspace.NttTransceiver.programId; -async function signSendWait( - chain: ChainContext, - txs: AsyncGenerator, - signer: Signer -) { - try { - await ssw(chain, txs, signer); - } catch (e) { - console.error(e); - } -} - +/** + * Test Helpers + */ +const $ = new TestHelper("confirmed", TOKEN_PROGRAM); +const testDummyTransferHook = new TestDummyTransferHook( + anchor.workspace.DummyTransferHook, + TOKEN_PROGRAM, + spl.ASSOCIATED_TOKEN_PROGRAM_ID +); +let testMint: TestMint; + +/** + * Wallet Config + */ +const payer = $.keypair.read(`${SOLANA_ROOT_DIR}/keys/test.json`); +const payerAddress = new SolanaAddress(payer.publicKey); + +/** + * Mint Config + */ +const mint = $.keypair.generate(); +const mintAuthority = $.keypair.generate(); + +/** + * Contract Config + */ const w = new Wormhole("Devnet", [SolanaPlatform], { chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } }, }); - -const remoteXcvr: ChainAddress = { - chain: "Ethereum", - address: new UniversalAddress( - encoding.bytes.encode("transceiver".padStart(32, "\0")) - ), -}; -const remoteMgr: ChainAddress = { - chain: "Ethereum", - address: new UniversalAddress( - encoding.bytes.encode("nttManager".padStart(32, "\0")) - ), -}; - -const payerSecretKey = Uint8Array.from( - JSON.parse( - fs.readFileSync(`${solanaRootDir}/keys/test.json`, { - encoding: "utf-8", - }) - ) -); -const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey); - -const owner = anchor.web3.Keypair.generate(); -const connection = new anchor.web3.Connection( - "http://localhost:8899", - "confirmed" -); - -// Make sure we're using the exact same Connection obj for rpc const ctx: ChainContext<"Devnet", "Solana"> = w .getPlatform("Solana") - .getChain("Solana", connection); - -let tokenAccount: anchor.web3.PublicKey; - -const mint = anchor.web3.Keypair.generate(); - -const dummyTransferHook = anchor.workspace - .DummyTransferHook as anchor.Program; - -const [extraAccountMetaListPDA] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], - dummyTransferHook.programId -); - -const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("counter")], - dummyTransferHook.programId -); - -async function counterValue(): Promise { - const counter = await dummyTransferHook.account.counter.fetch(counterPDA); - return counter.count; -} - -const coreBridge = new SolanaWormholeCore("Devnet", "Solana", connection, { + .getChain("Solana", $.connection); // make sure we're using the exact same Connection object for rpc +const coreBridge = new SolanaWormholeCore("Devnet", "Solana", $.connection, { coreBridge: CORE_BRIDGE_ADDRESS, }); - +const remoteMgr: ChainAddress = $.chainAddress.generateFromValue( + "Ethereum", + "nttManager" +); +const remoteXcvr: ChainAddress = $.chainAddress.generateFromValue( + "Ethereum", + "transceiver" +); const nttTransceivers = { wormhole: getTransceiverProgram( - connection, + $.connection, WH_TRANSCEIVER_ADDRESS.toBase58(), VERSION ), @@ -128,194 +101,116 @@ describe("example-native-token-transfers", () => { let ntt: SolanaNtt<"Devnet", "Solana">; let signer: Signer; let sender: AccountAddress<"Solana">; - let multisig: anchor.web3.PublicKey; - let tokenAddress: string; + let tokenAccount: anchor.web3.PublicKey; beforeAll(async () => { - try { - signer = await getSolanaSignAndSendSigner(connection, payer, { - //debug: true, - }); - sender = Wormhole.parseAddress("Solana", signer.address()); - - const extensions = [spl.ExtensionType.TransferHook]; - const mintLen = spl.getMintLen(extensions); - const lamports = await connection.getMinimumBalanceForRentExemption( - mintLen - ); - - const transaction = new anchor.web3.Transaction().add( - anchor.web3.SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TOKEN_PROGRAM, - }), - spl.createInitializeTransferHookInstruction( - mint.publicKey, - owner.publicKey, - dummyTransferHook.programId, - TOKEN_PROGRAM - ), - spl.createInitializeMintInstruction( - mint.publicKey, - 9, - owner.publicKey, - null, - TOKEN_PROGRAM - ) - ); - - const { blockhash } = await connection.getLatestBlockhash(); - - transaction.feePayer = payer.publicKey; - transaction.recentBlockhash = blockhash; - - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ - payer, - mint, - ]); - - tokenAccount = await spl.createAssociatedTokenAccount( - connection, - payer, - mint.publicKey, - payer.publicKey, - undefined, - TOKEN_PROGRAM, - spl.ASSOCIATED_TOKEN_PROGRAM_ID - ); - - await spl.mintTo( - connection, - payer, - mint.publicKey, - tokenAccount, - owner, - 10_000_000n, - undefined, - undefined, - TOKEN_PROGRAM - ); - - tokenAddress = mint.publicKey.toBase58(); - // Create our contract client - ntt = new SolanaNtt( - "Devnet", - "Solana", - connection, - { - ...ctx.config.contracts, - ntt: { - token: tokenAddress, - manager: NTT_ADDRESS.toBase58(), - transceiver: { - wormhole: nttTransceivers["wormhole"].programId.toBase58(), - }, + signer = await getSolanaSignAndSendSigner($.connection, payer, { + //debug: true, + }); + sender = Wormhole.parseAddress("Solana", signer.address()); + + testMint = await TestMint.createWithTokenExtensions( + $.connection, + payer, + mint, + mintAuthority, + 9, + TOKEN_PROGRAM, + spl.ASSOCIATED_TOKEN_PROGRAM_ID, + { + extensions: [spl.ExtensionType.TransferHook], + preMintInitIxs: [ + spl.createInitializeTransferHookInstruction( + mint.publicKey, + mintAuthority.publicKey, + testDummyTransferHook.program.programId, + TOKEN_PROGRAM + ), + ], + } + ); + + tokenAccount = await testMint.mint( + payer, + payer.publicKey, + 10_000_000n, + mintAuthority + ); + + // create our contract client + ntt = new SolanaNtt( + "Devnet", + "Solana", + $.connection, + { + ...ctx.config.contracts, + ntt: { + token: testMint.address.toBase58(), + manager: NTT_ADDRESS.toBase58(), + transceiver: { + wormhole: nttTransceivers["wormhole"].programId.toBase58(), }, }, - VERSION - ); - } catch (e) { - console.error("Failed to setup solana token: ", e); - throw e; - } + }, + VERSION + ); }); describe("Burning", () => { + let multisigTokenAuthority: anchor.web3.PublicKey; + beforeAll(async () => { - try { - multisig = await spl.createMultisig( - connection, - payer, - [owner.publicKey, ntt.pdas.tokenAuthority()], - 1, - anchor.web3.Keypair.generate(), - undefined, - TOKEN_PROGRAM - ); - await spl.setAuthority( - connection, - payer, - mint.publicKey, - owner, - spl.AuthorityType.MintTokens, - multisig, - [], - undefined, - TOKEN_PROGRAM - ); + // set multisigTokenAuthority as mint authority + multisigTokenAuthority = await $.multisig.create(payer, 1, [ + mintAuthority.publicKey, + ntt.pdas.tokenAuthority(), + ]); + await testMint.setMintAuthority( + payer, + multisigTokenAuthority, + mintAuthority + ); - // init - const initTxs = ntt.initialize(sender, { - mint: mint.publicKey, - outboundLimit: 1000000n, - mode: "burning", - multisig, - }); - await signSendWait(ctx, initTxs, signer); + // init + const initTxs = ntt.initialize(sender, { + mint: testMint.address, + outboundLimit: 1_000_000n, + mode: "burning", + multisig: multisigTokenAuthority, + }); + await signSendWait(ctx, initTxs, signer); - // register - const registerTxs = ntt.registerWormholeTransceiver({ - payer: new SolanaAddress(payer.publicKey), - owner: new SolanaAddress(payer.publicKey), - }); - await signSendWait(ctx, registerTxs, signer); + // register Wormhole xcvr + const registerTxs = ntt.registerWormholeTransceiver({ + payer: payerAddress, + owner: payerAddress, + }); + await signSendWait(ctx, registerTxs, signer); - // Set Wormhole xcvr peer - const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer( - remoteXcvr, - sender - ); - await signSendWait(ctx, setXcvrPeerTxs, signer); - - // Set manager peer - const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1000000n, sender); - await signSendWait(ctx, setPeerTxs, signer); - } catch (e) { - console.error("Failed to setup peer: ", e); - throw e; - } + // set Wormhole xcvr peer + const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer(remoteXcvr, sender); + await signSendWait(ctx, setXcvrPeerTxs, signer); + + // set manager peer + const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1_000_000n, sender); + await signSendWait(ctx, setPeerTxs, signer); }); it("Create ExtraAccountMetaList Account", async () => { - const initializeExtraAccountMetaListInstruction = - await dummyTransferHook.methods - .initializeExtraAccountMetaList() - .accountsStrict({ - payer: payer.publicKey, - mint: mint.publicKey, - counter: counterPDA, - extraAccountMetaList: extraAccountMetaListPDA, - tokenProgram: TOKEN_PROGRAM, - associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .instruction(); - - const transaction = new anchor.web3.Transaction().add( - initializeExtraAccountMetaListInstruction - ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - - transaction.sign(payer); - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + await testDummyTransferHook.extraAccountMetaList.initialize( + $.connection, payer, - ]); + testMint.address + ); }); - test("Can send tokens", async () => { - const amount = 100000n; - const sender = Wormhole.parseAddress("Solana", signer.address()); - + it("Can send tokens", async () => { + const amount = 100_000n; const receiver = testing.utils.makeUniversalChainAddress("Ethereum"); // TODO: keep or remove the `outboxItem` param? // added as a way to keep tests the same but it technically breaks the Ntt interface - const outboxItem = anchor.web3.Keypair.generate(); + const outboxItem = $.keypair.generate(); const xferTxs = ntt.transfer( sender, amount, @@ -333,11 +228,11 @@ describe("example-native-token-transfers", () => { Object.keys(nttTransceivers).length ); - const wormholeMessage = derivePda( - ["message", outboxItem.publicKey.toBytes()], - nttTransceivers["wormhole"].programId + const wormholeXcvr = await ntt.getWormholeTransceiver(); + expect(wormholeXcvr).toBeTruthy(); + const wormholeMessage = wormholeXcvr!.pdas.wormholeMessageAccount( + outboxItem.publicKey ); - const unsignedVaa = await coreBridge.parsePostMessageAccount( wormholeMessage ); @@ -350,11 +245,225 @@ describe("example-native-token-transfers", () => { // assert that amount is what we expect expect( transceiverMessage.nttManagerPayload.payload.trimmedAmount - ).toMatchObject({ amount: 10000n, decimals: 8 }); + ).toMatchObject({ amount: 10_000n, decimals: 8 }); // get from balance - const balance = await connection.getTokenAccountBalance(tokenAccount); - expect(balance.value.amount).toBe("9900000"); + await assert.tokenBalance($.connection, tokenAccount).equal(9_900_000); + }); + + describe("Can transfer mint authority to-and-from NTT manager", () => { + const newAuthority = $.keypair.generate(); + let newMultisigAuthority: anchor.web3.PublicKey; + const nttOwner = payer.publicKey; + + beforeAll(async () => { + newMultisigAuthority = await $.multisig.create(payer, 2, [ + mintAuthority.publicKey, + newAuthority.publicKey, + ]); + }); + + it("Fails when contract is not paused", async () => { + await assert + .promise( + $.sendAndConfirm( + await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( + ntt.program, + await ntt.getConfig(), + { + owner: nttOwner, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer + ) + ) + .failsWithAnchorError(anchor.web3.SendTransactionError, { + code: "NotPaused", + number: 6024, + }); + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); + }); + + test("Multisig(owner, TA) -> newAuthority", async () => { + // retry after pausing contract + const pauseTxs = ntt.pause(payerAddress); + await signSendWait(ctx, pauseTxs, signer); + + await $.sendAndConfirm( + await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( + ntt.program, + await ntt.getConfig(), + { + owner: nttOwner, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer + ); + + await assert.testMintAuthority(testMint).equal(newAuthority.publicKey); + }); + + test("newAuthority -> TA", async () => { + await $.sendAndConfirm( + await NTT.createAcceptTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + currentAuthority: newAuthority.publicKey, + } + ), + payer, + newAuthority + ); + + await assert + .testMintAuthority(testMint) + .equal(ntt.pdas.tokenAuthority()); + }); + + test("TA -> Multisig(owner, newAuthority)", async () => { + // set token authority: TA -> newMultisigAuthority + await $.sendAndConfirm( + await NTT.createSetTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: nttOwner, + owner: nttOwner, + newAuthority: newMultisigAuthority, + } + ), + payer + ); + + // claim token authority: newMultisigAuthority <- TA + await $.sendAndConfirm( + await NTT.createClaimTokenAuthorityToMultisigInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: nttOwner, + newMultisigAuthority, + additionalSigners: [ + newAuthority.publicKey, + mintAuthority.publicKey, + ], + } + ), + payer, + newAuthority, + mintAuthority + ); + + await assert.testMintAuthority(testMint).equal(newMultisigAuthority); + }); + + test("Multisig(owner, newAuthority) -> Multisig(owner, TA)", async () => { + await $.sendAndConfirm( + await NTT.createAcceptTokenAuthorityFromMultisigInstruction( + ntt.program, + await ntt.getConfig(), + { + currentMultisigAuthority: newMultisigAuthority, + additionalSigners: [ + newAuthority.publicKey, + mintAuthority.publicKey, + ], + multisigTokenAuthority, + } + ), + payer, + newAuthority, + mintAuthority + ); + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); + }); + + it("Fails on claim after revert", async () => { + // fund newAuthority for it to be rent payer + await $.airdrop(newAuthority.publicKey, anchor.web3.LAMPORTS_PER_SOL); + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal(anchor.web3.LAMPORTS_PER_SOL); + + // set token authority: multisigTokenAuthority -> newAuthority + await $.sendAndConfirm( + await NTT.createSetTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: nttOwner, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer, + newAuthority + ); + const pendingTokenAuthorityRentExemptAmount = + await $.connection.getMinimumBalanceForRentExemption( + ntt.program.account.pendingTokenAuthority.size + ); + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal( + anchor.web3.LAMPORTS_PER_SOL - pendingTokenAuthorityRentExemptAmount + ); + + // revert token authority: multisigTokenAuthority + await $.sendAndConfirm( + await NTT.createRevertTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: nttOwner, + multisigTokenAuthority, + } + ), + payer + ); + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal(anchor.web3.LAMPORTS_PER_SOL); + + // claim token authority: newAuthority <- multisigTokenAuthority + await assert + .promise( + $.sendAndConfirm( + await NTT.createClaimTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer, + newAuthority + ) + ) + .failsWithAnchorError(anchor.web3.SendTransactionError, { + code: "AccountNotInitialized", + number: 3012, + }); + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); + }); + + afterAll(async () => { + // unpause + const unpauseTxs = ntt.unpause(payerAddress); + await signSendWait(ctx, unpauseTxs, signer); + }); }); it("Can receive tokens", async () => { @@ -365,7 +474,6 @@ describe("example-native-token-transfers", () => { ); const guardians = new testing.mocks.MockGuardians(0, [GUARDIAN_KEY]); - const sender = Wormhole.parseAddress("Solana", signer.address()); const sendingTransceiverMessage = { sourceNttManager: remoteMgr.address as UniversalAddress, @@ -377,7 +485,7 @@ describe("example-native-token-transfers", () => { sender: new UniversalAddress("FACE".padStart(64, "0")), payload: { trimmedAmount: { - amount: 10000n, + amount: 10_000n, decimals: 8, }, sourceToken: new UniversalAddress("FAFA".padStart(64, "0")), @@ -396,41 +504,21 @@ describe("example-native-token-transfers", () => { const published = emitter.publishMessage(0, serialized, 200); const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); - const redeemTxs = ntt.redeem([vaa], sender, multisig); - try { - await signSendWait(ctx, redeemTxs, signer); - } catch (e) { - console.error(e); - throw e; - } + const redeemTxs = ntt.redeem([vaa], sender, multisigTokenAuthority); + await signSendWait(ctx, redeemTxs, signer); - expect((await counterValue()).toString()).toEqual("2"); + assert.bn(await testDummyTransferHook.counter.value()).equal(2); }); it("Can mint independently", async () => { - const dest = await spl.getOrCreateAssociatedTokenAccount( - connection, - payer, - mint.publicKey, - anchor.web3.Keypair.generate().publicKey, - false, - undefined, - undefined, - TOKEN_PROGRAM - ); - await spl.mintTo( - connection, + const temp = await testMint.mint( payer, - mint.publicKey, - dest.address, - multisig, + $.keypair.generate().publicKey, 1, - [owner], - undefined, - TOKEN_PROGRAM + multisigTokenAuthority, + mintAuthority ); - const balance = await connection.getTokenAccountBalance(dest.address); - expect(balance.value.amount.toString()).toBe("1"); + await assert.tokenBalance($.connection, temp).equal(1); }); }); @@ -439,7 +527,7 @@ describe("example-native-token-transfers", () => { const ctx = wh.getChain("Solana"); const overrides = { Solana: { - token: tokenAddress, + token: mint.publicKey.toBase58(), manager: NTT_ADDRESS.toBase58(), transceiver: { wormhole: nttTransceivers["wormhole"].programId.toBase58(), @@ -447,9 +535,9 @@ describe("example-native-token-transfers", () => { }, }; - describe("ABI Versions Test", function () { - test("It initializes from Rpc", async function () { - const ntt = await SolanaNtt.fromRpc(connection, { + describe("ABI Versions Test", () => { + test("It initializes from Rpc", async () => { + const ntt = await SolanaNtt.fromRpc($.connection, { Solana: { ...ctx.config, contracts: { @@ -461,24 +549,24 @@ describe("example-native-token-transfers", () => { expect(ntt).toBeTruthy(); }); - test("It initializes from constructor", async function () { - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + test("It initializes from constructor", async () => { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, }); expect(ntt).toBeTruthy(); }); - test("It gets the correct version", async function () { + test("It gets the correct version", async () => { const version = await SolanaNtt.getVersion( - connection, + $.connection, { ntt: overrides["Solana"] }, - new SolanaAddress(payer.publicKey.toBase58()) + payerAddress ); expect(version).toBe("3.0.0"); }); - test("It initializes using `emitterAccount` as transceiver address", async function () { + test("It initializes using `emitterAccount` as transceiver address", async () => { const overrideEmitter: (typeof overrides)["Solana"] = JSON.parse( JSON.stringify(overrides["Solana"]) ); @@ -486,21 +574,22 @@ describe("example-native-token-transfers", () => { .emitterAccount() .toBase58(); - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrideEmitter }, }); expect(ntt).toBeTruthy(); }); - test("It gets the correct transceiver type", async function () { - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + test("It gets the correct transceiver type", async () => { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, }); const whTransceiver = await ntt.getWormholeTransceiver(); + expect(whTransceiver).toBeTruthy(); const transceiverType = await whTransceiver!.getTransceiverType( - new SolanaAddress(payer.publicKey.toBase58()) + payerAddress ); expect(transceiverType).toBe("wormhole"); }); diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts new file mode 100644 index 000000000..784fe7b70 --- /dev/null +++ b/solana/tests/utils/helpers.ts @@ -0,0 +1,645 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as spl from "@solana/spl-token"; +import * as fs from "fs"; +import { + Chain, + ChainAddress, + ChainContext, + encoding, + Signer, + signSendWait as ssw, + UniversalAddress, +} from "@wormhole-foundation/sdk"; +import { DummyTransferHook } from "../../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; +import { derivePda } from "../../ts/lib/utils.js"; + +export interface ErrorConstructor { + new (...args: any[]): Error; +} + +/** + * Assertion utility functions + */ +export const assert = { + /** + * Asserts BN + * @param actual BN to compare against + */ + bn: (actual: anchor.BN) => ({ + /** + * Asserts `actual` equals `expected` + * @param expected BN to compare with + */ + equal: (expected: anchor.BN | number | string | bigint) => { + expect( + actual.eq( + expected instanceof anchor.BN + ? expected + : new anchor.BN(expected.toString()) + ) + ).toBeTruthy(); + }, + }), + + /** + * Asserts mint authority for given `mint` + * @param connection Connection to use + * @param mint Mint account + * @param tokenProgram SPL Token program account + */ + mintAuthority: ( + connection: anchor.web3.Connection, + mint: anchor.web3.PublicKey, + tokenProgram = spl.TOKEN_2022_PROGRAM_ID + ) => ({ + /** + * Asserts queried mint authority equals `expectedAuthority` + * @param expectedAuthority Expected mint authority + */ + equal: async (expectedAuthority: anchor.web3.PublicKey) => { + const mintInfo = await spl.getMint( + connection, + mint, + undefined, + tokenProgram + ); + expect(mintInfo.mintAuthority).toEqual(expectedAuthority); + }, + }), + + /** + * Asserts mint authority for given `testMint` + * @param testMint `TestMint` object to query to fetch mintAuthority + */ + testMintAuthority: (testMint: TestMint) => ({ + /** + * Asserts queried mint authority equals `expectedAuthority` + * @param expectedAuthority Expected mint authority + */ + equal: async (expectedAuthority: anchor.web3.PublicKey) => { + const mintInfo = await testMint.getMint(); + expect(mintInfo.mintAuthority).toEqual(expectedAuthority); + }, + }), + + /** + * Asserts native balance for given `publicKey` + * @param connection Connection to use + * @param publicKey Account to query to fetch native balance + * @returns + */ + nativeBalance: ( + connection: anchor.web3.Connection, + publicKey: anchor.web3.PublicKey + ) => ({ + /** + * Asserts queried native balance equals `expectedBalance` + * @param expectedBalance Expected lamports balance + */ + equal: async (expectedBalance: anchor.BN | number | string | bigint) => { + const balance = await connection.getAccountInfo(publicKey); + expect(balance?.lamports.toString()).toBe(expectedBalance.toString()); + }, + }), + + /** + * Asserts token balance for given `tokenAccount` + * @param connection Connection to use + * @param tokenAccount Token account to query to fetch token balance + */ + tokenBalance: ( + connection: anchor.web3.Connection, + tokenAccount: anchor.web3.PublicKey + ) => ({ + /** + * Asserts queried token balance equals `expectedBalance` + * @param expectedBalance Expected token balance + */ + equal: async (expectedBalance: anchor.BN | number | string | bigint) => { + const balance = await connection.getTokenAccountBalance(tokenAccount); + expect(balance.value.amount).toBe(expectedBalance.toString()); + }, + }), + + /** + * Asserts promise fails and throws expected error + * @param prom Promise to execute (intended to fail) + */ + promise: (prom: Promise) => ({ + /** + * Asserts promise throws error of type `errorType` + * @param errorType Expected type for thrown error + */ + fails: async (errorType?: ErrorConstructor) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + if (errorType != null) { + expect(error).toBeInstanceOf(errorType); + } + return; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + /** + * Asserts promise throws error containing `message` + * @param message Expected message contained in thrown error + */ + failsWith: async (message: string) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + const errorStr: string = error.toString(); + if (errorStr.includes(message)) { + return; + } + throw { + message: "Error does not contain the asked message", + stack: errorStr, + }; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + /** + * Asserts promise throws Anchor error coreesponding to type `errorType` and `errorCode` + * @param errorType Expected type for thrown error + * @param errorCode Expected error code for thrown error + */ + failsWithAnchorError: async ( + errorType: ErrorConstructor, + errorCode: typeof anchor.AnchorError.prototype.error.errorCode + ) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + expect(error).toBeInstanceOf(errorType); + const parsedError = anchor.AnchorError.parse(error.logs ?? []); + expect(parsedError?.error.errorCode).toEqual(errorCode); + return; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + }), +}; + +/** + * General test utility class + */ +export class TestHelper { + static readonly LOCALHOST = "http://localhost:8899"; + readonly connection: anchor.web3.Connection; + + constructor( + readonly finality: anchor.web3.Finality = "confirmed", + readonly tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID + ) { + this.connection = new anchor.web3.Connection( + TestHelper.LOCALHOST, + finality + ); + } + + /** + * `Keypair` utility functions + */ + keypair = { + /** + * Wrapper around `Keypair.generate()` + * @returns Generated `Keypair` + */ + generate: () => anchor.web3.Keypair.generate(), + /** + * Reads secret key file and returns `Keypair` it corresponds to + * @param path File path containing secret key + * @returns Corresponding `Keypair` + */ + read: (path: string) => + this.keypair.from( + JSON.parse(fs.readFileSync(path, { encoding: "utf8" })) + ), + /** + * Wrapper around `Keypair.fromSecretKey` for number array-like + * @param bytes Number array-like corresponding to a secret key + * @returns Corresponding `Keypair` + */ + from: (bytes: number[]) => + anchor.web3.Keypair.fromSecretKey(Uint8Array.from(bytes)), + }; + + /** + * `ChainAddress` utility functions + */ + chainAddress = { + /** + * Generates a `ChainAddress` by encoding value to pass off as `UniversalAddress` + * @param chain `Chain` to generate `ChainAddress` for + * @param value String to use for generating `UniversalAddress` + * @returns Generated `ChainAddress` + */ + generateFromValue: (chain: Chain, value: string): ChainAddress => ({ + chain, + address: new UniversalAddress( + encoding.bytes.encode(value.padStart(32, "\0")) + ), + }), + }; + + /** + * SPL Multisig utility functions + */ + multisig = { + /** + * Wrapper around `spl.createMultisig` + * @param payer Payer of the transaction and initialization fees + * @param m Number of required signatures + * @param signers Full set of signers + * @returns Address of the new multisig + */ + create: async ( + payer: anchor.web3.Signer, + m: number, + signers: anchor.web3.PublicKey[] + ) => { + return spl.createMultisig( + this.connection, + payer, + signers, + m, + this.keypair.generate(), + undefined, + this.tokenProgram + ); + }, + }; + + /** + * Wrapper around `confirmTransaction` + * @param signature Signature of transaction to confirm + * @returns Result of signature confirmation + */ + confirm = async (signature: anchor.web3.TransactionSignature) => { + const { blockhash, lastValidBlockHeight } = + await this.connection.getLatestBlockhash(); + return this.connection.confirmTransaction({ + blockhash, + lastValidBlockHeight, + signature, + }); + }; + + /** + * Wrapper around `sendAndConfirm` for `this.connection` + * @param ixs Instruction(s)/transaction used to create the transaction + * @param payer Payer of the transaction fees + * @param signers Signing accounts required by the transaction + * @returns Signature of the confirmed transaction + */ + sendAndConfirm = async ( + ixs: + | anchor.web3.TransactionInstruction + | anchor.web3.Transaction + | Array, + payer: anchor.web3.Signer, + ...signers: anchor.web3.Signer[] + ): Promise => { + return sendAndConfirm(this.connection, ixs, payer, ...signers); + }; + + /** + * Wrapper around `requestAirdrop()` + * @param to Recipient account for airdrop + * @param lamports Amount in lamports to airdrop + * @returns + */ + airdrop = async (to: anchor.web3.PublicKey, lamports: number) => { + return this.confirm(await this.connection.requestAirdrop(to, lamports)); + }; +} + +/** + * Mint-related test utility class + */ +export class TestMint { + private constructor( + readonly connection: anchor.web3.Connection, + readonly address: anchor.web3.PublicKey, + readonly decimals: number, + readonly tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + readonly associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) {} + + /** + * Creates and initializes a new mint + * @param connection Connection to use + * @param payer Payer of the transaction and initialization fees + * @param authority Account that will control minting + * @param decimals Location of the decimal place + * @param tokenProgram SPL Token program account + * @param associatedTokenProgram SPL Associated Token program account + * @returns new `TestMint` object initialized with the created mint + */ + static create = async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + authority: anchor.web3.Signer, + decimals: number, + tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) => { + return new TestMint( + connection, + await spl.createMint( + connection, + payer, + authority.publicKey, + null, + decimals, + undefined, + undefined, + tokenProgram + ), + decimals, + tokenProgram, + associatedTokenProgram + ); + }; + + /** + * Creates and initializes a new mint with Token Extensions + * @param connection Connection to use + * @param payer Payer of the transaction and initialization fees + * @param mint Keypair of mint to be created + * @param authority Account that will control minting + * @param decimals Location of the decimal place + * @param tokenProgram SPL Token program account + * @param associatedTokenProgram SPL Associated Token program account + * @param extensionArgs.extensions Token extensions mint is to be initialized with + * @param extensionArgs.additionalDataLength Additional space to allocate for extension + * @param extensionArgs.preMintInitIxs Instructions to execute before `InitializeMint` instruction + * @param extensionArgs.postMintInitIxs Instructions to execute after `InitializeMint` instruction + * @returns new `TestMint` object initialized with the created mint + */ + static createWithTokenExtensions = async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + mint: anchor.web3.Keypair, + authority: anchor.web3.Signer, + decimals: number, + tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID, + extensionArgs: { + extensions: spl.ExtensionType[]; + additionalDataLength?: number; + preMintInitIxs?: anchor.web3.TransactionInstruction[]; + postMintInitIxs?: anchor.web3.TransactionInstruction[]; + } + ) => { + const mintLen = spl.getMintLen(extensionArgs.extensions); + const additionalDataLength = extensionArgs.additionalDataLength ?? 0; + const lamports = await connection.getMinimumBalanceForRentExemption( + mintLen + additionalDataLength + ); + await sendAndConfirm( + connection, + [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports, + programId: tokenProgram, + }), + ...(extensionArgs.preMintInitIxs ?? []), + spl.createInitializeMintInstruction( + mint.publicKey, + decimals, + authority.publicKey, + null, + tokenProgram + ), + ...(extensionArgs.postMintInitIxs ?? []), + ], + payer, + mint + ); + + return new TestMint( + connection, + mint.publicKey, + decimals, + tokenProgram, + associatedTokenProgram + ); + }; + + /** + * Wrapper around `spl.getMint` + * @returns Mint information + */ + getMint = async () => { + return spl.getMint( + this.connection, + this.address, + undefined, + this.tokenProgram + ); + }; + + /** + * Creates ATA for `accountOwner` and mints `amount` tokens to it + * @param payer Payer of the transaction and initialization fees + * @param accountOwner Owner of token account + * @param amount Amount to mint + * @param mintAuthority Minting authority + * @param multiSigners Signing accounts if `mintAuthority` is a multisig + * @returns Address of ATA + */ + mint = async ( + payer: anchor.web3.Signer, + accountOwner: anchor.web3.PublicKey, + amount: number | bigint, + mintAuthority: anchor.web3.Signer | anchor.web3.PublicKey, + ...multiSigners: anchor.web3.Signer[] + ) => { + const tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + this.connection, + payer, + this.address, + accountOwner, + false, + undefined, + undefined, + this.tokenProgram, + this.associatedTokenProgram + ); + + await spl.mintTo( + this.connection, + payer, + this.address, + tokenAccount.address, + mintAuthority, + amount, + multiSigners, + undefined, + this.tokenProgram + ); + + return tokenAccount.address; + }; + + /** + * Wrapper around `spl.setAuthority` for `spl.AuthorityType.MintTokens` + * @param payer Payer of the transaction fees + * @param newAuthority New mint authority + * @param currentAuthority Current mint authority + * @param multiSigners Signing accounts if `currentAuthority` is a multisig + * @returns Signature of the confirmed transaction + */ + setMintAuthority = async ( + payer: anchor.web3.Signer, + newAuthority: anchor.web3.PublicKey, + currentAuthority: anchor.web3.Signer | anchor.web3.PublicKey, + ...multiSigners: anchor.web3.Signer[] + ) => { + return spl.setAuthority( + this.connection, + payer, + this.address, + currentAuthority, + spl.AuthorityType.MintTokens, + newAuthority, + multiSigners, + undefined, + this.tokenProgram + ); + }; +} + +/** + * Dummy Transfer Hook program related test utility class + */ +export class TestDummyTransferHook { + constructor( + readonly program: anchor.Program, + readonly tokenProgram = spl.TOKEN_2022_PROGRAM_ID, + readonly associatedTokenProgram = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) {} + + /** + * Counter utility functions + */ + counter = { + /** + * @returns Counter PDA + */ + pda: () => derivePda(["counter"], this.program.programId), + + /** + * Queries counter and returns counter count + * @returns Queried counter value + */ + value: async () => { + const counter = await this.program.account.counter.fetch( + this.counter.pda() + ); + return counter.count; + }, + }; + + /** + * Extra Account Meta List utility functions + */ + extraAccountMetaList = { + /** + * @param mint Mint account + * @returns Extra Account Meta List PDA + */ + pda: (mint: anchor.web3.PublicKey) => + derivePda( + ["extra-account-metas", mint.toBytes()], + this.program.programId + ), + /** + * Initializes Extra Account Meta List account + * @param connection Connection to use + * @param payer Payer of the transaction fees + * @param mint Mint account + * @returns Signature of the confirmed transaction + */ + initialize: async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + mint: anchor.web3.PublicKey + ) => { + return sendAndConfirm( + connection, + await this.program.methods + .initializeExtraAccountMetaList() + .accountsStrict({ + payer: payer.publicKey, + mint, + counter: this.counter.pda(), + extraAccountMetaList: this.extraAccountMetaList.pda(mint), + tokenProgram: this.tokenProgram, + associatedTokenProgram: this.associatedTokenProgram, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .instruction(), + payer + ); + }, + }; +} + +/** + * Try-catch wrapper around `signSendWait` + * @param chain Chain to execute transaction on + * @param txs Generator of unsigned transactions + * @param signer Signing account required by the transactions + */ +export const signSendWait = async ( + chain: ChainContext, + txs: AsyncGenerator, + signer: Signer +) => { + try { + await ssw(chain, txs, signer); + } catch (e) { + console.error(e); + } +}; + +/** + * Wrapper around `sendAndConfirmTransaction` + * @param connection Connection to use + * @param ixs Instruction(s)/transaction used to create the transaction + * @param payer Payer of the transaction fees + * @param signers Signing accounts required by the transaction + * @returns Signature of the confirmed transaction + */ +export const sendAndConfirm = async ( + connection: anchor.web3.Connection, + ixs: + | anchor.web3.TransactionInstruction + | anchor.web3.Transaction + | Array, + payer: anchor.web3.Signer, + ...signers: anchor.web3.Signer[] +): Promise => { + const { value } = await connection.getLatestBlockhashAndContext(); + const tx = new anchor.web3.Transaction({ + ...value, + feePayer: payer.publicKey, + }).add(...(Array.isArray(ixs) ? ixs : [ixs])); + + return anchor.web3.sendAndConfirmTransaction( + connection, + tx, + [payer, ...signers], + {} + ); +}; diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index ea99bdc9e..457cfd5f8 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -892,24 +892,124 @@ "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -945,6 +1045,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -971,7 +1077,7 @@ "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -982,37 +1088,53 @@ "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -1032,6 +1154,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1055,7 +1183,7 @@ ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -1063,7 +1191,7 @@ "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -1083,6 +1211,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1106,9 +1240,9 @@ ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 30ccc2364..e0305fc6b 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -892,24 +892,124 @@ export type ExampleNativeTokenTransfers = { "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -945,6 +1045,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -971,7 +1077,7 @@ export type ExampleNativeTokenTransfers = { "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -982,37 +1088,53 @@ export type ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -1032,6 +1154,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1055,7 +1183,7 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -1063,7 +1191,7 @@ export type ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -1083,6 +1211,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1106,9 +1240,9 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] @@ -3350,24 +3484,124 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -3403,6 +3637,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -3429,7 +3669,7 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -3440,37 +3680,53 @@ export const IDL: ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -3490,6 +3746,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -3513,7 +3775,7 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -3521,7 +3783,7 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -3541,6 +3803,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -3564,9 +3832,9 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 4aed49162..26d0f2d59 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -849,6 +849,7 @@ export namespace NTT { config: NttBindings.Config, args: { currentAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -856,15 +857,78 @@ export namespace NTT { return program.methods .acceptTokenAuthority() .accountsStrict({ - config: pdas.configAccount(), - mint: config.mint, - tokenProgram: config.tokenProgram, - tokenAuthority: pdas.tokenAuthority(), + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenProgram: config.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, currentAuthority: args.currentAuthority, }) .instruction(); } + export async function createAcceptTokenAuthorityFromMultisigInstruction( + program: Program>, + config: NttBindings.Config, + args: { + currentMultisigAuthority: PublicKey; + additionalSigners: readonly PublicKey[]; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .acceptTokenAuthorityFromMultisig() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenProgram: config.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + currentMultisigAuthority: args.currentMultisigAuthority, + }) + .remainingAccounts( + args.additionalSigners.map((pubkey) => ({ + pubkey, + isSigner: true, + isWritable: false, + })) + ) + .instruction(); + } + + export async function createSetTokenAuthorityOneStepUncheckedInstruction( + program: Program>, + config: NttBindings.Config, + args: { + owner: PublicKey; + newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .setTokenAuthorityOneStepUnchecked() + .accountsStrict({ + common: { + config: pdas.configAccount(), + tokenAuthority: pdas.tokenAuthority(), + mint: config.mint, + owner: args.owner, + newAuthority: args.newAuthority, + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + tokenProgram: config.tokenProgram, + }) + .instruction(); + } + export async function createSetTokenAuthorityInstruction( program: Program>, config: NttBindings.Config, @@ -872,6 +936,7 @@ export namespace NTT { rentPayer: PublicKey; owner: PublicKey; newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -885,6 +950,7 @@ export namespace NTT { mint: config.mint, owner: args.owner, newAuthority: args.newAuthority, + multisigTokenAuthority: args.multisigTokenAuthority ?? null, }, rentPayer: args.rentPayer, pendingTokenAuthority: pdas.pendingTokenAuthority(), @@ -899,6 +965,7 @@ export namespace NTT { args: { rentPayer: PublicKey; owner: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -914,12 +981,79 @@ export namespace NTT { systemProgram: SystemProgram.programId, rentPayer: args.rentPayer, pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, }, owner: args.owner, }) .instruction(); } + export async function createClaimTokenAuthorityInstruction( + program: Program>, + config: NttBindings.Config, + args: { + rentPayer: PublicKey; + newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .claimTokenAuthority() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenAuthority: pdas.tokenAuthority(), + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, + rentPayer: args.rentPayer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + newAuthority: args.newAuthority, + }) + .instruction(); + } + + export async function createClaimTokenAuthorityToMultisigInstruction( + program: Program>, + config: NttBindings.Config, + args: { + rentPayer: PublicKey; + newMultisigAuthority: PublicKey; + additionalSigners: readonly PublicKey[]; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .claimTokenAuthorityToMultisig() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenAuthority: pdas.tokenAuthority(), + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, + rentPayer: args.rentPayer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + newMultisigAuthority: args.newMultisigAuthority, + }) + .remainingAccounts( + args.additionalSigners.map((pubkey) => ({ + pubkey, + isSigner: true, + isWritable: false, + })) + ) + .instruction(); + } + export async function createSetPeerInstruction( program: Program>, args: {