"""SPL token instructions."""  # pylint: disable=too-many-lines

from enum import IntEnum
from typing import Any, List, NamedTuple, Optional, Union

from solana.publickey import PublicKey
from solana.system_program import SYS_PROGRAM_ID
from solana.sysvar import SYSVAR_RENT_PUBKEY
from solana.transaction import AccountMeta, TransactionInstruction
from solana.utils.validate import validate_instruction_keys, validate_instruction_type
from spl.token._layouts import INSTRUCTIONS_LAYOUT, InstructionType
from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID


class AuthorityType(IntEnum):
    """Specifies the authority type for SetAuthority instructions."""

    MINT_TOKENS = 0
    """"Authority to mint new tokens."""
    FREEZE_ACCOUNT = 1
    """Authority to freeze any account associated with the Mint."""
    ACCOUNT_OWNER = 2
    """Owner of a given token account."""
    CLOSE_ACCOUNT = 3
    """Authority to close a token account."""


# Instruction Params
class InitializeMintParams(NamedTuple):
    """Initialize token mint transaction params."""

    decimals: int
    """Number of base 10 digits to the right of the decimal place."""
    program_id: PublicKey
    """SPL Token program account."""
    mint: PublicKey
    """Public key of the minter account."""
    mint_authority: PublicKey
    """The authority/multisignature to mint tokens."""
    freeze_authority: Optional[PublicKey] = None
    """The freeze authority/multisignature of the mint."""


class InitializeAccountParams(NamedTuple):
    """Initialize token account transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Public key of the new account."""
    mint: PublicKey
    """Public key of the minter account."""
    owner: PublicKey
    """Owner of the new account."""


class InitializeMultisigParams(NamedTuple):
    """Initialize multisig token account transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    multisig: PublicKey
    """New multisig account address."""
    m: int
    """The number of signers (M) required to validate this multisignature account."""
    signers: List[PublicKey] = []
    """Addresses of multisig signers."""


class TransferParams(NamedTuple):
    """Transfer token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    source: PublicKey
    """Source account."""
    dest: PublicKey
    """Destination account."""
    owner: PublicKey
    """Owner of the source account."""
    amount: int
    """Number of tokens to transfer."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig."""


class ApproveParams(NamedTuple):
    """Approve token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    source: PublicKey
    """Source account."""
    delegate: PublicKey
    """Delegate account authorized to perform a transfer of tokens from the source account."""
    owner: PublicKey
    """Owner of the source account."""
    amount: int
    """Maximum number of tokens the delegate may transfer."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig."""


class RevokeParams(NamedTuple):
    """Revoke token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Source account for which transfer authority is being revoked."""
    owner: PublicKey
    """Owner of the source account."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig."""


class SetAuthorityParams(NamedTuple):
    """Set token authority transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Public key of the token account."""
    authority: AuthorityType
    """The type of authority to update."""
    current_authority: PublicKey
    """Current authority of the specified type."""
    signers: List[PublicKey] = []
    """Signing accounts if `current_authority` is a multiSig."""
    new_authority: Optional[PublicKey] = None
    """New authority of the account."""


class MintToParams(NamedTuple):
    """Mint token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    mint: PublicKey
    """Public key of the minter account."""
    dest: PublicKey
    """Public key of the account to mint to."""
    mint_authority: PublicKey
    """The mint authority."""
    amount: int
    """Amount to mint."""
    signers: List[PublicKey] = []
    """Signing accounts if `mint_authority` is a multiSig."""


class BurnParams(NamedTuple):
    """Burn token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Account to burn tokens from."""
    mint: PublicKey
    """Public key of the minter account."""
    owner: PublicKey
    """Owner of the account."""
    amount: int
    """Amount to burn."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig"""


class CloseAccountParams(NamedTuple):
    """Close token account transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Address of account to close."""
    dest: PublicKey
    """Address of account to receive the remaining balance of the closed account."""
    owner: PublicKey
    """Owner of the account."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig"""


class FreezeAccountParams(NamedTuple):
    """Freeze token account transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Account to freeze."""
    mint: PublicKey
    """Public key of the minter account."""
    authority: PublicKey
    """Mint freeze authority"""
    multi_signers: List[PublicKey] = []
    """Signing accounts if `authority` is a multiSig"""


class ThawAccountParams(NamedTuple):
    """Thaw token account transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    account: PublicKey
    """Account to thaw."""
    mint: PublicKey
    """Public key of the minter account."""
    authority: PublicKey
    """Mint freeze authority"""
    multi_signers: List[PublicKey] = []
    """Signing accounts if `authority` is a multiSig"""


class TransferCheckedParams(NamedTuple):
    """TransferChecked token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    source: PublicKey
    """Source account."""
    mint: PublicKey
    """Public key of the minter account."""
    dest: PublicKey
    """Destination account."""
    owner: PublicKey
    """Owner of the source account."""
    amount: int
    """Number of tokens to transfer."""
    decimals: int
    """Amount decimals."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig."""


class ApproveCheckedParams(NamedTuple):
    """ApproveChecked token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    source: PublicKey
    """Source account."""
    mint: PublicKey
    """Public key of the minter account."""
    delegate: PublicKey
    """Delegate account authorized to perform a transfer of tokens from the source account."""
    owner: PublicKey
    """Owner of the source account."""
    amount: int
    """Maximum number of tokens the delegate may transfer."""
    decimals: int
    """Amount decimals."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig."""


class MintToCheckedParams(NamedTuple):
    """MintToChecked token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    mint: PublicKey
    """Public key of the minter account."""
    dest: PublicKey
    """Public key of the account to mint to."""
    mint_authority: PublicKey
    """The mint authority."""
    amount: int
    """Amount to mint."""
    decimals: int
    """Amount decimals."""
    signers: List[PublicKey] = []
    """Signing accounts if `mint_authority` is a multiSig."""


class BurnCheckedParams(NamedTuple):
    """BurnChecked token transaction params."""

    program_id: PublicKey
    """SPL Token program account."""
    mint: PublicKey
    """Public key of the minter account."""
    account: PublicKey
    """Account to burn tokens from."""
    owner: PublicKey
    """Owner of the account."""
    amount: int
    """Amount to burn."""
    decimals: int
    """Amount decimals."""
    signers: List[PublicKey] = []
    """Signing accounts if `owner` is a multiSig"""


def __parse_and_validate_instruction(
    instruction: TransactionInstruction,
    expected_keys: int,
    expected_type: InstructionType,
) -> Any:  # Returns a Construct container.
    validate_instruction_keys(instruction, expected_keys)
    data = INSTRUCTIONS_LAYOUT.parse(instruction.data)
    validate_instruction_type(data, expected_type)
    return data


def decode_initialize_mint(instruction: TransactionInstruction) -> InitializeMintParams:
    """Decode an initialize mint token instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 2, InstructionType.INITIALIZE_MINT)
    return InitializeMintParams(
        decimals=parsed_data.args.decimals,
        program_id=instruction.program_id,
        mint=instruction.keys[0].pubkey,
        mint_authority=PublicKey(parsed_data.args.mint_authority),
        freeze_authority=PublicKey(parsed_data.args.freeze_authority)
        if parsed_data.args.freeze_authority_option
        else None,
    )


def decode_initialize_account(instruction: TransactionInstruction) -> InitializeAccountParams:
    """Decode an initialize account token instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    _ = __parse_and_validate_instruction(instruction, 4, InstructionType.INITIALIZE_ACCOUNT)
    return InitializeAccountParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
    )


def decode_initialize_multisig(instruction: TransactionInstruction) -> InitializeMultisigParams:
    """Decode an initialize multisig account token instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 2, InstructionType.INITIALIZE_MULTISIG)
    num_signers = parsed_data.args.m
    validate_instruction_keys(instruction, 2 + num_signers)
    return InitializeMultisigParams(
        program_id=instruction.program_id,
        multisig=instruction.keys[0].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[-num_signers:]],
        m=num_signers,
    )


def decode_transfer(instruction: TransactionInstruction) -> TransferParams:
    """Decode a transfer token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.TRANSFER)
    return TransferParams(
        program_id=instruction.program_id,
        source=instruction.keys[0].pubkey,
        dest=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
        amount=parsed_data.args.amount,
    )


def decode_approve(instruction: TransactionInstruction) -> ApproveParams:
    """Decode a approve token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.APPROVE)
    return ApproveParams(
        program_id=instruction.program_id,
        source=instruction.keys[0].pubkey,
        delegate=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
        amount=parsed_data.args.amount,
    )


def decode_revoke(instruction: TransactionInstruction) -> RevokeParams:
    """Decode a revoke token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    _ = __parse_and_validate_instruction(instruction, 2, InstructionType.REVOKE)
    return RevokeParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        owner=instruction.keys[1].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[2:]],
    )


def decode_set_authority(instruction: TransactionInstruction) -> SetAuthorityParams:
    """Decode a set authority token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 2, InstructionType.SET_AUTHORITY)
    return SetAuthorityParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        authority=AuthorityType(parsed_data.args.authority_type),
        new_authority=PublicKey(parsed_data.args.new_authority) if parsed_data.args.new_authority_option else None,
        current_authority=instruction.keys[1].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[2:]],
    )


def decode_mint_to(instruction: TransactionInstruction) -> MintToParams:
    """Decode a mint to token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.MINT_TO)
    return MintToParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        mint=instruction.keys[0].pubkey,
        dest=instruction.keys[1].pubkey,
        mint_authority=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_burn(instruction: TransactionInstruction) -> BurnParams:
    """Decode a burn token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.BURN)
    return BurnParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        account=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_close_account(instruction: TransactionInstruction) -> CloseAccountParams:
    """Decode a close account token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    _ = __parse_and_validate_instruction(instruction, 3, InstructionType.CLOSE_ACCOUNT)
    return CloseAccountParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        dest=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_freeze_account(instruction: TransactionInstruction) -> FreezeAccountParams:
    """Decode a freeze account token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    _ = __parse_and_validate_instruction(instruction, 3, InstructionType.FREEZE_ACCOUNT)
    return FreezeAccountParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        authority=instruction.keys[2].pubkey,
        multi_signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_thaw_account(instruction: TransactionInstruction) -> ThawAccountParams:
    """Decode a thaw account token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    _ = __parse_and_validate_instruction(instruction, 3, InstructionType.THAW_ACCOUNT)
    return ThawAccountParams(
        program_id=instruction.program_id,
        account=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        authority=instruction.keys[2].pubkey,
        multi_signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_transfer_checked(instruction: TransactionInstruction) -> TransferCheckedParams:
    """Decode a transfer_checked token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 4, InstructionType.TRANSFER2)
    return TransferCheckedParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        decimals=parsed_data.args.decimals,
        source=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        dest=instruction.keys[2].pubkey,
        owner=instruction.keys[3].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[4:]],
    )


def decode_approve_checked(instruction: TransactionInstruction) -> ApproveCheckedParams:
    """Decode a approve_checked token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 4, InstructionType.APPROVE2)
    return ApproveCheckedParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        decimals=parsed_data.args.decimals,
        source=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        delegate=instruction.keys[2].pubkey,
        owner=instruction.keys[3].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[4:]],
    )


def decode_mint_to_checked(instruction: TransactionInstruction) -> MintToCheckedParams:
    """Decode a mintTo2 token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.MINT_TO2)
    return MintToCheckedParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        decimals=parsed_data.args.decimals,
        mint=instruction.keys[0].pubkey,
        dest=instruction.keys[1].pubkey,
        mint_authority=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def decode_burn_checked(instruction: TransactionInstruction) -> BurnCheckedParams:
    """Decode a burn_checked token transaction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 3, InstructionType.BURN2)
    return BurnCheckedParams(
        program_id=instruction.program_id,
        amount=parsed_data.args.amount,
        decimals=parsed_data.args.decimals,
        account=instruction.keys[0].pubkey,
        mint=instruction.keys[1].pubkey,
        owner=instruction.keys[2].pubkey,
        signers=[signer.pubkey for signer in instruction.keys[3:]],
    )


def __add_signers(keys: List[AccountMeta], owner: PublicKey, signers: List[PublicKey]) -> None:
    if signers:
        keys.append(AccountMeta(pubkey=owner, is_signer=False, is_writable=False))
        for signer in signers:
            keys.append(AccountMeta(pubkey=signer, is_signer=True, is_writable=False))
    else:
        keys.append(AccountMeta(pubkey=owner, is_signer=True, is_writable=False))


def __burn_instruction(params: Union[BurnParams, BurnCheckedParams], data: Any) -> TransactionInstruction:
    keys = [
        AccountMeta(pubkey=params.account, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.mint, is_signer=False, is_writable=True),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def __freeze_or_thaw_instruction(
    params: Union[FreezeAccountParams, ThawAccountParams], instruction_type: InstructionType
) -> TransactionInstruction:
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=instruction_type, args=None))
    keys = [
        AccountMeta(pubkey=params.account, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.mint, is_signer=False, is_writable=False),
    ]
    __add_signers(keys, params.authority, params.multi_signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def __mint_to_instruction(params: Union[MintToParams, MintToCheckedParams], data: Any) -> TransactionInstruction:
    keys = [
        AccountMeta(pubkey=params.mint, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.dest, is_signer=False, is_writable=True),
    ]
    __add_signers(keys, params.mint_authority, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def initialize_mint(params: InitializeMintParams) -> TransactionInstruction:
    """Creates a transaction instruction to initialize a new mint newly.

    This instruction requires no signers and MUST be included within the same Transaction as
    the system program's `CreateInstruction` that creates the account being initialized.
    Otherwise another party can acquire ownership of the uninitialized account.

    Example:

        >>> from spl.token.constants import TOKEN_PROGRAM_ID
        >>> mint_account, mint_authority, freeze_authority, owner = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = InitializeMintParams(
        ...     decimals=6,
        ...     freeze_authority=freeze_authority,
        ...     mint=mint_account,
        ...     mint_authority=mint_authority,
        ...     program_id=TOKEN_PROGRAM_ID,
        ... )
        >>> type(initialize_mint(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The instruction to initialize the mint.
    """  # noqa: E501 # pylint: disable=line-too-long
    freeze_authority, opt = (params.freeze_authority, 1) if params.freeze_authority else (PublicKey(0), 0)
    data = INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.INITIALIZE_MINT,
            args=dict(
                decimals=params.decimals,
                mint_authority=bytes(params.mint_authority),
                freeze_authority_option=opt,
                freeze_authority=bytes(freeze_authority),
            ),
        )
    )
    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.mint, is_signer=False, is_writable=True),
            AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
        ],
        program_id=params.program_id,
        data=data,
    )


def initialize_account(params: InitializeAccountParams) -> TransactionInstruction:
    """Creates a transaction instruction to initialize a new account to hold tokens.

    This instruction requires no signers and MUST be included within the same Transaction as
    the system program's `CreateInstruction` that creates the account being initialized.
    Otherwise another party can acquire ownership of the uninitialized account.

    Example:

        >>> account, mint, owner, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = InitializeAccountParams(
        ...     account=account,
        ...     mint=mint,
        ...     owner=owner,
        ...     program_id=token,
        ... )
        >>> type(initialize_account(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The instruction to initialize the account.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.INITIALIZE_ACCOUNT, args=None))
    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.account, is_signer=False, is_writable=True),
            AccountMeta(pubkey=params.mint, is_signer=False, is_writable=False),
            AccountMeta(pubkey=params.owner, is_signer=False, is_writable=False),
            AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
        ],
        program_id=params.program_id,
        data=data,
    )


def initialize_multisig(params: InitializeMultisigParams) -> TransactionInstruction:
    """Creates a transaction instruction to initialize a multisignature account with N provided signers.

    This instruction requires no signers and MUST be included within the same Transaction as
    the system program's `CreateInstruction` that creates the account being initialized.
    Otherwise another party can acquire ownership of the uninitialized account.

    Example:

        >>> m = 2   # Two signers
        >>> signers = [PublicKey(i) for i in range(m)]
        >>> multisig_account, token = PublicKey(1), PublicKey(2)
        >>> params = InitializeMultisigParams(
        ...     m=m,
        ...     multisig=multisig_account,
        ...     signers=signers,
        ...     program_id=token,
        ... )
        >>> type(initialize_multisig(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The instruction to initialize the multisig.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.INITIALIZE_MULTISIG, args=dict(m=params.m)))
    keys = [
        AccountMeta(pubkey=params.multisig, is_signer=False, is_writable=True),
        AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
    ]
    for signer in params.signers:
        keys.append(AccountMeta(pubkey=signer, is_signer=False, is_writable=False))

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def transfer(params: TransferParams) -> TransactionInstruction:
    """Creates a transaction instruction to transfers tokens from one account to another.

    Either directly or via a delegate.

    Example:

        >>> dest, owner, source, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = TransferParams(
        ...     amount=1000,
        ...     dest=dest,
        ...     owner=owner,
        ...     program_id=token,
        ...     source=source,
        ... )
        >>> type(transfer(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The transfer instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.TRANSFER, args=dict(amount=params.amount)))
    keys = [
        AccountMeta(pubkey=params.source, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.dest, is_signer=False, is_writable=True),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def approve(params: ApproveParams) -> TransactionInstruction:
    """Creates a transaction instruction to approve a delegate.

    Example:

        >>> delegate, owner, source, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = ApproveParams(
        ...     amount=123,
        ...     delegate=delegate,
        ...     owner=owner,
        ...     program_id=token,
        ...     source=source
        ... )
        >>> type(approve(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The approve instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.APPROVE, args=dict(amount=params.amount)))
    keys = [
        AccountMeta(pubkey=params.source, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.delegate, is_signer=False, is_writable=False),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def revoke(params: RevokeParams) -> TransactionInstruction:
    """Creates a transaction instruction that revokes delegate authority for a given account.

    Example:

        >>> account, owner, token = PublicKey(1), PublicKey(2), PublicKey(3)
        >>> params = RevokeParams(
        ...     account=account, owner=owner, program_id=token
        ... )
        >>> type(revoke(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The revoke instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.REVOKE, args=None))
    keys = [AccountMeta(pubkey=params.account, is_signer=False, is_writable=True)]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def set_authority(params: SetAuthorityParams) -> TransactionInstruction:
    """Creates a transaction instruction to sets a new authority of a mint or account.

    Example:

        >>> account, current_authority, new_authority, token = (
        ...     PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        ... )
        >>> params = SetAuthorityParams(
        ...     account=account,
        ...     authority=AuthorityType.ACCOUNT_OWNER,
        ...     current_authority=current_authority,
        ...     new_authority=new_authority,
        ...     program_id=token,
        ... )
        >>> type(set_authority(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The set authority instruction.
    """
    new_authority, opt = (params.new_authority, 1) if params.new_authority else (PublicKey(0), 0)
    data = INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.SET_AUTHORITY,
            args=dict(authority_type=params.authority, new_authority_option=opt, new_authority=bytes(new_authority)),
        )
    )
    keys = [AccountMeta(pubkey=params.account, is_signer=False, is_writable=True)]
    __add_signers(keys, params.current_authority, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def mint_to(params: MintToParams) -> TransactionInstruction:
    """Creates a transaction instruction to mint new tokens to an account.

    The native mint does not support minting.

    Example:

        >>> dest, mint, mint_authority, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = MintToParams(
        ...     amount=123,
        ...     dest=dest,
        ...     mint=mint,
        ...     mint_authority=mint_authority,
        ...     program_id=token,
        ... )
        >>> type(mint_to(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The mint-to instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.MINT_TO, args=dict(amount=params.amount)))
    return __mint_to_instruction(params, data)


def burn(params: BurnParams) -> TransactionInstruction:
    """Creates a transaction instruction to burns tokens by removing them from an account.

    Example:

        >>> account, mint, owner, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = BurnParams(
        ...     amount=123, account=account, mint=mint, owner=owner, program_id=token,
        ... )
        >>> type(burn(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The burn instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.BURN, args=dict(amount=params.amount)))
    return __burn_instruction(params, data)


def close_account(params: CloseAccountParams) -> TransactionInstruction:
    """Creates a transaction instruction to close an account by transferring all its SOL to the destination account.

    Non-native accounts may only be closed if its token amount is zero.

    Example:

        >>> account, dest, owner, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = CloseAccountParams(
        ...     account=account, dest=dest, owner=owner, program_id=token)
        >>> type(close_account(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The close-account instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(dict(instruction_type=InstructionType.CLOSE_ACCOUNT, args=None))
    keys = [
        AccountMeta(pubkey=params.account, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.dest, is_signer=False, is_writable=True),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def freeze_account(params: FreezeAccountParams) -> TransactionInstruction:
    """Creates a transaction instruction to freeze an initialized account using the mint's freeze_authority (if set).

    Example:

        >>> account, mint, authority, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = FreezeAccountParams(
        ...     account=account, mint=mint, authority=authority, program_id=token)
        >>> type(freeze_account(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The freeze-account instruction.
    """
    return __freeze_or_thaw_instruction(params, InstructionType.FREEZE_ACCOUNT)


def thaw_account(params: ThawAccountParams) -> TransactionInstruction:
    """Creates a transaction instruction to thaw a frozen account using the Mint's freeze_authority (if set).

    Example:

        >>> account, mint, authority, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = ThawAccountParams(
        ...     account=account, mint=mint, authority=authority, program_id=token)
        >>> type(thaw_account(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The thaw-account instruction.
    """
    return __freeze_or_thaw_instruction(params, InstructionType.THAW_ACCOUNT)


def transfer_checked(params: TransferCheckedParams) -> TransactionInstruction:
    """This instruction differs from `transfer` in that the token mint and decimals value is asserted by the caller.

    Example:

        >>> dest, mint, owner, source, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4), PublicKey(5)
        >>> params = TransferCheckedParams(
        ...     amount=1000,
        ...     decimals=6,
        ...     dest=dest,
        ...     mint=mint,
        ...     owner=owner,
        ...     program_id=token,
        ...     source=source,
        ... )
        >>> type(transfer_checked(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The transfer-checked instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.TRANSFER2, args=dict(amount=params.amount, decimals=params.decimals))
    )
    keys = [
        AccountMeta(pubkey=params.source, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.mint, is_signer=False, is_writable=False),
        AccountMeta(pubkey=params.dest, is_signer=False, is_writable=True),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def approve_checked(params: ApproveCheckedParams) -> TransactionInstruction:
    """This instruction differs from `approve` in that the token mint and decimals value is asserted by the caller.

    Example:

        >>> delegate, mint, owner, source, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4), PublicKey(5)
        >>> params = ApproveCheckedParams(
        ...     amount=1000,
        ...     decimals=6,
        ...     delegate=delegate,
        ...     mint=mint,
        ...     owner=owner,
        ...     program_id=token,
        ...     source=source,
        ... )
        >>> type(approve_checked(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The approve-checked instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.APPROVE2, args=dict(amount=params.amount, decimals=params.decimals))
    )
    keys = [
        AccountMeta(pubkey=params.source, is_signer=False, is_writable=True),
        AccountMeta(pubkey=params.mint, is_signer=False, is_writable=False),
        AccountMeta(pubkey=params.delegate, is_signer=False, is_writable=False),
    ]
    __add_signers(keys, params.owner, params.signers)

    return TransactionInstruction(keys=keys, program_id=params.program_id, data=data)


def mint_to_checked(params: MintToCheckedParams) -> TransactionInstruction:
    """This instruction differs from `mint_to` in that the decimals value is asserted by the caller.

    Example:

        >>> dest, mint, mint_authority, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = MintToCheckedParams(
        ...     amount=123,
        ...     decimals=6,
        ...     dest=dest,
        ...     mint=mint,
        ...     mint_authority=mint_authority,
        ...     program_id=token,
        ... )
        >>> type(mint_to_checked(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The mint-to-checked instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.MINT_TO2, args=dict(amount=params.amount, decimals=params.decimals))
    )
    return __mint_to_instruction(params, data)


def burn_checked(params: BurnCheckedParams) -> TransactionInstruction:
    """This instruction differs from `burn` in that the decimals value is asserted by the caller.

    Example:

        >>> account, mint, owner, token = PublicKey(1), PublicKey(2), PublicKey(3), PublicKey(4)
        >>> params = BurnCheckedParams(
        ...     amount=123, account=account, decimals=6, mint=mint, owner=owner, program_id=token,
        ... )
        >>> type(burn_checked(params))
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The burn-checked instruction.
    """
    data = INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.BURN2, args=dict(amount=params.amount, decimals=params.decimals))
    )
    return __burn_instruction(params, data)


def get_associated_token_address(owner: PublicKey, mint: PublicKey) -> PublicKey:
    """Derives the associated token address for the given wallet address and token mint.

    Returns:
        The public key of the derived associated token address.
    """
    key, _ = PublicKey.find_program_address(
        seeds=[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], program_id=ASSOCIATED_TOKEN_PROGRAM_ID
    )
    return key


def create_associated_token_account(payer: PublicKey, owner: PublicKey, mint: PublicKey) -> TransactionInstruction:
    """Creates a transaction instruction to create an associated token account.

    Returns:
        The instruction to create the associated token account.
    """
    associated_token_address = get_associated_token_address(owner, mint)
    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=payer, is_signer=True, is_writable=True),
            AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True),
            AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
            AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
            AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
            AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False),
            AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
        ],
        program_id=ASSOCIATED_TOKEN_PROGRAM_ID,
    )
