/**
 * @flow
 */

import {Buffer} from 'buffer';
import assert from 'assert';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import {
  Account,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
  SYSVAR_RENT_PUBKEY,
} from '@solana/web3.js';
import type {
  Connection,
  Commitment,
  TransactionSignature,
} from '@solana/web3.js';

import * as Layout from './layout';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';

export const TOKEN_PROGRAM_ID: PublicKey = new PublicKey(
  'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);

export const ASSOCIATED_TOKEN_PROGRAM_ID: PublicKey = new PublicKey(
  'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
);

const FAILED_TO_FIND_ACCOUNT = 'Failed to find account';
const INVALID_ACCOUNT_OWNER = 'Invalid account owner';

/**
 * Unfortunately, BufferLayout.encode uses an `instanceof` check for `Buffer`
 * which fails when using `publicKey.toBuffer()` directly because the bundled `Buffer`
 * class in `@solana/web3.js` is different from the bundled `Buffer` class in this package
 */
function pubkeyToBuffer(publicKey: PublicKey): typeof Buffer {
  return Buffer.from(publicKey.toBuffer());
}

/**
 * 64-bit value
 */
export class u64 extends BN {
  /**
   * Convert to Buffer representation
   */
  toBuffer(): typeof Buffer {
    const a = super.toArray().reverse();
    const b = Buffer.from(a);
    if (b.length === 8) {
      return b;
    }
    assert(b.length < 8, 'u64 too large');

    const zeroPad = Buffer.alloc(8);
    b.copy(zeroPad);
    return zeroPad;
  }

  /**
   * Construct a u64 from Buffer representation
   */
  static fromBuffer(buffer: typeof Buffer): u64 {
    assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
    return new u64(
      [...buffer]
        .reverse()
        .map(i => `00${i.toString(16)}`.slice(-2))
        .join(''),
      16,
    );
  }
}

function isAccount(accountOrPublicKey: any): boolean {
  return 'publicKey' in accountOrPublicKey;
}

type AuthorityType =
  | 'MintTokens'
  | 'FreezeAccount'
  | 'AccountOwner'
  | 'CloseAccount';

const AuthorityTypeCodes = {
  MintTokens: 0,
  FreezeAccount: 1,
  AccountOwner: 2,
  CloseAccount: 3,
};

// The address of the special mint for wrapped native token.
export const NATIVE_MINT: PublicKey = new PublicKey(
  'So11111111111111111111111111111111111111112',
);

/**
 * Information about the mint
 */
type MintInfo = {|
  /**
   * Optional authority used to mint new tokens. The mint authority may only be provided during
   * mint creation. If no mint authority is present then the mint has a fixed supply and no
   * further tokens may be minted.
   */
  mintAuthority: null | PublicKey,

  /**
   * Total supply of tokens
   */
  supply: u64,

  /**
   * Number of base 10 digits to the right of the decimal place
   */
  decimals: number,

  /**
   * Is this mint initialized
   */
  isInitialized: boolean,

  /**
   * Optional authority to freeze token accounts
   */
  freezeAuthority: null | PublicKey,
|};

export const MintLayout: typeof BufferLayout.Structure = BufferLayout.struct([
  BufferLayout.u32('mintAuthorityOption'),
  Layout.publicKey('mintAuthority'),
  Layout.uint64('supply'),
  BufferLayout.u8('decimals'),
  BufferLayout.u8('isInitialized'),
  BufferLayout.u32('freezeAuthorityOption'),
  Layout.publicKey('freezeAuthority'),
]);

/**
 * Information about an account
 */
type AccountInfo = {|
  /**
   * The address of this account
   */
  address: PublicKey,

  /**
   * The mint associated with this account
   */
  mint: PublicKey,

  /**
   * Owner of this account
   */
  owner: PublicKey,

  /**
   * Amount of tokens this account holds
   */
  amount: u64,

  /**
   * The delegate for this account
   */
  delegate: null | PublicKey,

  /**
   * The amount of tokens the delegate authorized to the delegate
   */
  delegatedAmount: u64,

  /**
   * Is this account initialized
   */
  isInitialized: boolean,

  /**
   * Is this account frozen
   */
  isFrozen: boolean,

  /**
   * Is this a native token account
   */
  isNative: boolean,

  /**
   * If this account is a native token, it must be rent-exempt. This
   * value logs the rent-exempt reserve which must remain in the balance
   * until the account is closed.
   */
  rentExemptReserve: null | u64,

  /**
   * Optional authority to close the account
   */
  closeAuthority: null | PublicKey,
|};

/**
 * @private
 */
export const AccountLayout: typeof BufferLayout.Structure = BufferLayout.struct(
  [
    Layout.publicKey('mint'),
    Layout.publicKey('owner'),
    Layout.uint64('amount'),
    BufferLayout.u32('delegateOption'),
    Layout.publicKey('delegate'),
    BufferLayout.u8('state'),
    BufferLayout.u32('isNativeOption'),
    Layout.uint64('isNative'),
    Layout.uint64('delegatedAmount'),
    BufferLayout.u32('closeAuthorityOption'),
    Layout.publicKey('closeAuthority'),
  ],
);

/**
 * Information about an multisig
 */
type MultisigInfo = {|
  /**
   * The number of signers required
   */
  m: number,

  /**
   * Number of possible signers, corresponds to the
   * number of `signers` that are valid.
   */
  n: number,

  /**
   * Is this mint initialized
   */
  initialized: boolean,

  /**
   * The signers
   */
  signer1: PublicKey,
  signer2: PublicKey,
  signer3: PublicKey,
  signer4: PublicKey,
  signer5: PublicKey,
  signer6: PublicKey,
  signer7: PublicKey,
  signer8: PublicKey,
  signer9: PublicKey,
  signer10: PublicKey,
  signer11: PublicKey,
|};

/**
 * @private
 */
const MultisigLayout = BufferLayout.struct([
  BufferLayout.u8('m'),
  BufferLayout.u8('n'),
  BufferLayout.u8('is_initialized'),
  Layout.publicKey('signer1'),
  Layout.publicKey('signer2'),
  Layout.publicKey('signer3'),
  Layout.publicKey('signer4'),
  Layout.publicKey('signer5'),
  Layout.publicKey('signer6'),
  Layout.publicKey('signer7'),
  Layout.publicKey('signer8'),
  Layout.publicKey('signer9'),
  Layout.publicKey('signer10'),
  Layout.publicKey('signer11'),
]);

/**
 * An ERC20-like Token
 */
export class Token {
  /**
   * @private
   */
  connection: Connection;

  /**
   * The public key identifying this mint
   */
  publicKey: PublicKey;

  /**
   * Program Identifier for the Token program
   */
  programId: PublicKey;

  /**
   * Program Identifier for the Associated Token program
   */
  associatedProgramId: PublicKey;

  /**
   * Fee payer
   */
  payer: Account;

  /**
   * Create a Token object attached to the specific mint
   *
   * @param connection The connection to use
   * @param token Public key of the mint
   * @param programId token programId
   * @param payer Payer of fees
   */
  constructor(
    connection: Connection,
    publicKey: PublicKey,
    programId: PublicKey,
    payer: Account,
  ) {
    Object.assign(this, {
      connection,
      publicKey,
      programId,
      payer,
      // Hard code is ok; Overriding is needed only for tests
      associatedProgramId: ASSOCIATED_TOKEN_PROGRAM_ID,
    });
  }

  /**
   * Get the minimum balance for the mint to be rent exempt
   *
   * @return Number of lamports required
   */
  static async getMinBalanceRentForExemptMint(
    connection: Connection,
  ): Promise<number> {
    return await connection.getMinimumBalanceForRentExemption(MintLayout.span);
  }

  /**
   * Get the minimum balance for the account to be rent exempt
   *
   * @return Number of lamports required
   */
  static async getMinBalanceRentForExemptAccount(
    connection: Connection,
  ): Promise<number> {
    return await connection.getMinimumBalanceForRentExemption(
      AccountLayout.span,
    );
  }

  /**
   * Get the minimum balance for the multsig to be rent exempt
   *
   * @return Number of lamports required
   */
  static async getMinBalanceRentForExemptMultisig(
    connection: Connection,
  ): Promise<number> {
    return await connection.getMinimumBalanceForRentExemption(
      MultisigLayout.span,
    );
  }

  /**
   * Create and initialize a token.
   *
   * @param connection The connection to use
   * @param payer Fee payer for transaction
   * @param mintAuthority Account or multisig that will control minting
   * @param freezeAuthority Optional account or multisig that can freeze token accounts
   * @param decimals Location of the decimal place
   * @param programId Optional token programId, uses the system programId by default
   * @return Token object for the newly minted token
   */
  static async createMint(
    connection: Connection,
    payer: Account,
    mintAuthority: PublicKey,
    freezeAuthority: PublicKey | null,
    decimals: number,
    programId: PublicKey,
  ): Promise<Token> {
    const mintAccount = new Account();
    const token = new Token(
      connection,
      mintAccount.publicKey,
      programId,
      payer,
    );

    // Allocate memory for the account
    const balanceNeeded = await Token.getMinBalanceRentForExemptMint(
      connection,
    );

    const transaction = new Transaction();
    transaction.add(
      SystemProgram.createAccount({
        fromPubkey: payer.publicKey,
        newAccountPubkey: mintAccount.publicKey,
        lamports: balanceNeeded,
        space: MintLayout.span,
        programId,
      }),
    );

    transaction.add(
      Token.createInitMintInstruction(
        programId,
        mintAccount.publicKey,
        decimals,
        mintAuthority,
        freezeAuthority,
      ),
    );

    // Send the two instructions
    await sendAndConfirmTransaction(
      'createAccount and InitializeMint',
      connection,
      transaction,
      payer,
      mintAccount,
    );

    return token;
  }

  /**
   * Create and initialize a new account.
   *
   * This account may then be used as a `transfer()` or `approve()` destination
   *
   * @param owner User account that will own the new account
   * @return Public key of the new empty account
   */
  async createAccount(owner: PublicKey): Promise<PublicKey> {
    // Allocate memory for the account
    const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(
      this.connection,
    );

    const newAccount = new Account();
    const transaction = new Transaction();
    transaction.add(
      SystemProgram.createAccount({
        fromPubkey: this.payer.publicKey,
        newAccountPubkey: newAccount.publicKey,
        lamports: balanceNeeded,
        space: AccountLayout.span,
        programId: this.programId,
      }),
    );

    const mintPublicKey = this.publicKey;
    transaction.add(
      Token.createInitAccountInstruction(
        this.programId,
        mintPublicKey,
        newAccount.publicKey,
        owner,
      ),
    );

    // Send the two instructions
    await sendAndConfirmTransaction(
      'createAccount and InitializeAccount',
      this.connection,
      transaction,
      this.payer,
      newAccount,
    );

    return newAccount.publicKey;
  }

  /**
   * Create and initialize the associated account.
   *
   * This account may then be used as a `transfer()` or `approve()` destination
   *
   * @param owner User account that will own the new account
   * @return Public key of the new associated account
   */
  async createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey> {
    const associatedAddress = await Token.getAssociatedTokenAddress(
      this.associatedProgramId,
      this.programId,
      this.publicKey,
      owner,
    );

    return this.createAssociatedTokenAccountInternal(owner, associatedAddress);
  }

  async createAssociatedTokenAccountInternal(
    owner: PublicKey,
    associatedAddress: PublicKey,
  ): Promise<PublicKey> {
    await sendAndConfirmTransaction(
      'CreateAssociatedTokenAccount',
      this.connection,
      new Transaction().add(
        Token.createAssociatedTokenAccountInstruction(
          this.associatedProgramId,
          this.programId,
          this.publicKey,
          associatedAddress,
          owner,
          this.payer.publicKey,
        ),
      ),
      this.payer,
    );

    return associatedAddress;
  }

  /**
   * Retrieve the associated account or create one if not found.
   *
   * This account may then be used as a `transfer()` or `approve()` destination
   *
   * @param owner User account that will own the new account
   * @return The new associated account
   */
  async getOrCreateAssociatedAccountInfo(
    owner: PublicKey,
  ): Promise<AccountInfo> {
    const associatedAddress = await Token.getAssociatedTokenAddress(
      this.associatedProgramId,
      this.programId,
      this.publicKey,
      owner,
    );

    // This is the optimum logic, considering TX fee, client-side computation,
    // RPC roundtrips and guaranteed idempotent.
    // Sadly we can't do this atomically;
    try {
      return await this.getAccountInfo(associatedAddress);
    } catch (err) {
      // INVALID_ACCOUNT_OWNER can be possible if the associatedAddress has
      // already been received some lamports (= became system accounts).
      // Assuming program derived addressing is safe, this is the only case
      // for the INVALID_ACCOUNT_OWNER in this code-path
      if (
        err.message === FAILED_TO_FIND_ACCOUNT ||
        err.message === INVALID_ACCOUNT_OWNER
      ) {
        // as this isn't atomic, it's possible others can create associated
        // accounts meanwhile
        try {
          await this.createAssociatedTokenAccountInternal(
            owner,
            associatedAddress,
          );
        } catch (err) {
          // ignore all errors; for now there is no API compatible way to
          // selectively ignore the expected instruction error if the
          // associated account is existing already.
        }

        // Now this should always succeed
        return await this.getAccountInfo(associatedAddress);
      } else {
        throw err;
      }
    }
  }

  /**
   * Create and initialize a new account on the special native token mint.
   *
   * In order to be wrapped, the account must have a balance of native tokens
   * when it is initialized with the token program.
   *
   * This function sends lamports to the new account before initializing it.
   *
   * @param connection A solana web3 connection
   * @param programId The token program ID
   * @param owner The owner of the new token account
   * @param payer The source of the lamports to initialize, and payer of the initialization fees.
   * @param amount The amount of lamports to wrap
   * @return {Promise<PublicKey>} The new token account
   */
  static async createWrappedNativeAccount(
    connection: Connection,
    programId: PublicKey,
    owner: PublicKey,
    payer: Account,
    amount: number,
  ): Promise<PublicKey> {
    // Allocate memory for the account
    const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(
      connection,
    );

    // Create a new account
    const newAccount = new Account();
    const transaction = new Transaction();
    transaction.add(
      SystemProgram.createAccount({
        fromPubkey: payer.publicKey,
        newAccountPubkey: newAccount.publicKey,
        lamports: balanceNeeded,
        space: AccountLayout.span,
        programId,
      }),
    );

    // Send lamports to it (these will be wrapped into native tokens by the token program)
    transaction.add(
      SystemProgram.transfer({
        fromPubkey: payer.publicKey,
        toPubkey: newAccount.publicKey,
        lamports: amount,
      }),
    );

    // Assign the new account to the native token mint.
    // the account will be initialized with a balance equal to the native token balance.
    // (i.e. amount)
    transaction.add(
      Token.createInitAccountInstruction(
        programId,
        NATIVE_MINT,
        newAccount.publicKey,
        owner,
      ),
    );

    // Send the three instructions
    await sendAndConfirmTransaction(
      'createAccount, transfer, and initializeAccount',
      connection,
      transaction,
      payer,
      newAccount,
    );

    return newAccount.publicKey;
  }

  /**
   * Create and initialize a new multisig.
   *
   * This account may then be used for multisignature verification
   *
   * @param m Number of required signatures
   * @param signers Full set of signers
   * @return Public key of the new multisig account
   */
  async createMultisig(
    m: number,
    signers: Array<PublicKey>,
  ): Promise<PublicKey> {
    const multisigAccount = new Account();

    // Allocate memory for the account
    const balanceNeeded = await Token.getMinBalanceRentForExemptMultisig(
      this.connection,
    );
    const transaction = new Transaction();
    transaction.add(
      SystemProgram.createAccount({
        fromPubkey: this.payer.publicKey,
        newAccountPubkey: multisigAccount.publicKey,
        lamports: balanceNeeded,
        space: MultisigLayout.span,
        programId: this.programId,
      }),
    );

    // create the new account
    let keys = [
      {pubkey: multisigAccount.publicKey, isSigner: false, isWritable: true},
      {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
    ];
    signers.forEach(signer =>
      keys.push({pubkey: signer, isSigner: false, isWritable: false}),
    );
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      BufferLayout.u8('m'),
    ]);
    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 2, // InitializeMultisig instruction
        m,
      },
      data,
    );
    transaction.add({
      keys,
      programId: this.programId,
      data,
    });

    // Send the two instructions
    await sendAndConfirmTransaction(
      'createAccount and InitializeMultisig',
      this.connection,
      transaction,
      this.payer,
      multisigAccount,
    );

    return multisigAccount.publicKey;
  }

  /**
   * Retrieve mint information
   */
  async getMintInfo(): Promise<MintInfo> {
    const info = await this.connection.getAccountInfo(this.publicKey);
    if (info === null) {
      throw new Error('Failed to find mint account');
    }
    if (!info.owner.equals(this.programId)) {
      throw new Error(`Invalid mint owner: ${JSON.stringify(info.owner)}`);
    }
    if (info.data.length != MintLayout.span) {
      throw new Error(`Invalid mint size`);
    }

    const data = Buffer.from(info.data);
    const mintInfo = MintLayout.decode(data);

    if (mintInfo.mintAuthorityOption === 0) {
      mintInfo.mintAuthority = null;
    } else {
      mintInfo.mintAuthority = new PublicKey(mintInfo.mintAuthority);
    }

    mintInfo.supply = u64.fromBuffer(mintInfo.supply);
    mintInfo.isInitialized = mintInfo.isInitialized != 0;

    if (mintInfo.freezeAuthorityOption === 0) {
      mintInfo.freezeAuthority = null;
    } else {
      mintInfo.freezeAuthority = new PublicKey(mintInfo.freezeAuthority);
    }
    return mintInfo;
  }

  /**
   * Retrieve account information
   *
   * @param account Public key of the account
   */
  async getAccountInfo(
    account: PublicKey,
    commitment?: Commitment,
  ): Promise<AccountInfo> {
    const info = await this.connection.getAccountInfo(account, commitment);
    if (info === null) {
      throw new Error(FAILED_TO_FIND_ACCOUNT);
    }
    if (!info.owner.equals(this.programId)) {
      throw new Error(INVALID_ACCOUNT_OWNER);
    }
    if (info.data.length != AccountLayout.span) {
      throw new Error(`Invalid account size`);
    }

    const data = Buffer.from(info.data);
    const accountInfo = AccountLayout.decode(data);
    accountInfo.address = account;
    accountInfo.mint = new PublicKey(accountInfo.mint);
    accountInfo.owner = new PublicKey(accountInfo.owner);
    accountInfo.amount = u64.fromBuffer(accountInfo.amount);

    if (accountInfo.delegateOption === 0) {
      accountInfo.delegate = null;
      accountInfo.delegatedAmount = new u64();
    } else {
      accountInfo.delegate = new PublicKey(accountInfo.delegate);
      accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount);
    }

    accountInfo.isInitialized = accountInfo.state !== 0;
    accountInfo.isFrozen = accountInfo.state === 2;

    if (accountInfo.isNativeOption === 1) {
      accountInfo.rentExemptReserve = u64.fromBuffer(accountInfo.isNative);
      accountInfo.isNative = true;
    } else {
      accountInfo.rentExemptReserve = null;
      accountInfo.isNative = false;
    }

    if (accountInfo.closeAuthorityOption === 0) {
      accountInfo.closeAuthority = null;
    } else {
      accountInfo.closeAuthority = new PublicKey(accountInfo.closeAuthority);
    }

    if (!accountInfo.mint.equals(this.publicKey)) {
      throw new Error(
        `Invalid account mint: ${JSON.stringify(
          accountInfo.mint,
        )} !== ${JSON.stringify(this.publicKey)}`,
      );
    }
    return accountInfo;
  }

  /**
   * Retrieve Multisig information
   *
   * @param multisig Public key of the account
   */
  async getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo> {
    const info = await this.connection.getAccountInfo(multisig);
    if (info === null) {
      throw new Error('Failed to find multisig');
    }
    if (!info.owner.equals(this.programId)) {
      throw new Error(`Invalid multisig owner`);
    }
    if (info.data.length != MultisigLayout.span) {
      throw new Error(`Invalid multisig size`);
    }

    const data = Buffer.from(info.data);
    const multisigInfo = MultisigLayout.decode(data);
    multisigInfo.signer1 = new PublicKey(multisigInfo.signer1);
    multisigInfo.signer2 = new PublicKey(multisigInfo.signer2);
    multisigInfo.signer3 = new PublicKey(multisigInfo.signer3);
    multisigInfo.signer4 = new PublicKey(multisigInfo.signer4);
    multisigInfo.signer5 = new PublicKey(multisigInfo.signer5);
    multisigInfo.signer6 = new PublicKey(multisigInfo.signer6);
    multisigInfo.signer7 = new PublicKey(multisigInfo.signer7);
    multisigInfo.signer8 = new PublicKey(multisigInfo.signer8);
    multisigInfo.signer9 = new PublicKey(multisigInfo.signer9);
    multisigInfo.signer10 = new PublicKey(multisigInfo.signer10);
    multisigInfo.signer11 = new PublicKey(multisigInfo.signer11);

    return multisigInfo;
  }

  /**
   * Transfer tokens to another account
   *
   * @param source Source account
   * @param destination Destination account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Number of tokens to transfer
   */
  async transfer(
    source: PublicKey,
    destination: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): Promise<TransactionSignature> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    return await sendAndConfirmTransaction(
      'Transfer',
      this.connection,
      new Transaction().add(
        Token.createTransferInstruction(
          this.programId,
          source,
          destination,
          ownerPublicKey,
          multiSigners,
          amount,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Grant a third-party permission to transfer up the specified number of tokens from an account
   *
   * @param account Public key of the account
   * @param delegate Account authorized to perform a transfer tokens from the source account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Maximum number of tokens the delegate may transfer
   */
  async approve(
    account: PublicKey,
    delegate: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'Approve',
      this.connection,
      new Transaction().add(
        Token.createApproveInstruction(
          this.programId,
          account,
          delegate,
          ownerPublicKey,
          multiSigners,
          amount,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Remove approval for the transfer of any remaining tokens
   *
   * @param account Public key of the account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   */
  async revoke(
    account: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'Revoke',
      this.connection,
      new Transaction().add(
        Token.createRevokeInstruction(
          this.programId,
          account,
          ownerPublicKey,
          multiSigners,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Assign a new authority to the account
   *
   * @param account Public key of the account
   * @param newAuthority New authority of the account
   * @param authorityType Type of authority to set
   * @param currentAuthority Current authority of the account
   * @param multiSigners Signing accounts if `currentAuthority` is a multiSig
   */
  async setAuthority(
    account: PublicKey,
    newAuthority: PublicKey | null,
    authorityType: AuthorityType,
    currentAuthority: any,
    multiSigners: Array<Account>,
  ): Promise<void> {
    let currentAuthorityPublicKey: PublicKey;
    let signers;
    if (isAccount(currentAuthority)) {
      currentAuthorityPublicKey = currentAuthority.publicKey;
      signers = [currentAuthority];
    } else {
      currentAuthorityPublicKey = currentAuthority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'SetAuthority',
      this.connection,
      new Transaction().add(
        Token.createSetAuthorityInstruction(
          this.programId,
          account,
          newAuthority,
          authorityType,
          currentAuthorityPublicKey,
          multiSigners,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Mint new tokens
   *
   * @param dest Public key of the account to mint to
   * @param authority Minting authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Amount to mint
   */
  async mintTo(
    dest: PublicKey,
    authority: any,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(authority)) {
      ownerPublicKey = authority.publicKey;
      signers = [authority];
    } else {
      ownerPublicKey = authority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'MintTo',
      this.connection,
      new Transaction().add(
        Token.createMintToInstruction(
          this.programId,
          this.publicKey,
          dest,
          ownerPublicKey,
          multiSigners,
          amount,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Burn tokens
   *
   * @param account Account to burn tokens from
   * @param owner Account owner
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Amount to burn
   */
  async burn(
    account: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'Burn',
      this.connection,
      new Transaction().add(
        Token.createBurnInstruction(
          this.programId,
          this.publicKey,
          account,
          ownerPublicKey,
          multiSigners,
          amount,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Close account
   *
   * @param account Account to close
   * @param dest Account to receive the remaining balance of the closed account
   * @param authority Authority which is allowed to close the account
   * @param multiSigners Signing accounts if `authority` is a multiSig
   */
  async closeAccount(
    account: PublicKey,
    dest: PublicKey,
    authority: any,
    multiSigners: Array<Account>,
  ): Promise<void> {
    let authorityPublicKey;
    let signers;
    if (isAccount(authority)) {
      authorityPublicKey = authority.publicKey;
      signers = [authority];
    } else {
      authorityPublicKey = authority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'CloseAccount',
      this.connection,
      new Transaction().add(
        Token.createCloseAccountInstruction(
          this.programId,
          account,
          dest,
          authorityPublicKey,
          multiSigners,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Freeze account
   *
   * @param account Account to freeze
   * @param authority The mint freeze authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   */
  async freezeAccount(
    account: PublicKey,
    authority: any,
    multiSigners: Array<Account>,
  ): Promise<void> {
    let authorityPublicKey;
    let signers;
    if (isAccount(authority)) {
      authorityPublicKey = authority.publicKey;
      signers = [authority];
    } else {
      authorityPublicKey = authority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'FreezeAccount',
      this.connection,
      new Transaction().add(
        Token.createFreezeAccountInstruction(
          this.programId,
          account,
          this.publicKey,
          authorityPublicKey,
          multiSigners,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Thaw account
   *
   * @param account Account to thaw
   * @param authority The mint freeze authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   */
  async thawAccount(
    account: PublicKey,
    authority: any,
    multiSigners: Array<Account>,
  ): Promise<void> {
    let authorityPublicKey;
    let signers;
    if (isAccount(authority)) {
      authorityPublicKey = authority.publicKey;
      signers = [authority];
    } else {
      authorityPublicKey = authority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'ThawAccount',
      this.connection,
      new Transaction().add(
        Token.createThawAccountInstruction(
          this.programId,
          account,
          this.publicKey,
          authorityPublicKey,
          multiSigners,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Transfer tokens to another account, asserting the token mint and decimals
   *
   * @param source Source account
   * @param destination Destination account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Number of tokens to transfer
   * @param decimals Number of decimals in transfer amount
   */
  async transferChecked(
    source: PublicKey,
    destination: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): Promise<TransactionSignature> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    return await sendAndConfirmTransaction(
      'TransferChecked',
      this.connection,
      new Transaction().add(
        Token.createTransferCheckedInstruction(
          this.programId,
          source,
          this.publicKey,
          destination,
          ownerPublicKey,
          multiSigners,
          amount,
          decimals,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Grant a third-party permission to transfer up the specified number of tokens from an account,
   * asserting the token mint and decimals
   *
   * @param account Public key of the account
   * @param delegate Account authorized to perform a transfer tokens from the source account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Maximum number of tokens the delegate may transfer
   * @param decimals Number of decimals in approve amount
   */
  async approveChecked(
    account: PublicKey,
    delegate: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'ApproveChecked',
      this.connection,
      new Transaction().add(
        Token.createApproveCheckedInstruction(
          this.programId,
          account,
          this.publicKey,
          delegate,
          ownerPublicKey,
          multiSigners,
          amount,
          decimals,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Mint new tokens, asserting the token mint and decimals
   *
   * @param dest Public key of the account to mint to
   * @param authority Minting authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Amount to mint
   * @param decimals Number of decimals in amount to mint
   */
  async mintToChecked(
    dest: PublicKey,
    authority: any,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(authority)) {
      ownerPublicKey = authority.publicKey;
      signers = [authority];
    } else {
      ownerPublicKey = authority;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'MintToChecked',
      this.connection,
      new Transaction().add(
        Token.createMintToCheckedInstruction(
          this.programId,
          this.publicKey,
          dest,
          ownerPublicKey,
          multiSigners,
          amount,
          decimals,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Burn tokens, asserting the token mint and decimals
   *
   * @param account Account to burn tokens from
   * @param owner Account owner
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Amount to burn
   * @param decimals Number of decimals in amount to burn
   */
  async burnChecked(
    account: PublicKey,
    owner: any,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): Promise<void> {
    let ownerPublicKey;
    let signers;
    if (isAccount(owner)) {
      ownerPublicKey = owner.publicKey;
      signers = [owner];
    } else {
      ownerPublicKey = owner;
      signers = multiSigners;
    }
    await sendAndConfirmTransaction(
      'BurnChecked',
      this.connection,
      new Transaction().add(
        Token.createBurnCheckedInstruction(
          this.programId,
          this.publicKey,
          account,
          ownerPublicKey,
          multiSigners,
          amount,
          decimals,
        ),
      ),
      this.payer,
      ...signers,
    );
  }

  /**
   * Construct an InitializeMint instruction
   *
   * @param programId SPL Token program account
   * @param mint Token mint account
   * @param decimals Number of decimals in token account amounts
   * @param mintAuthority Minting authority
   * @param freezeAuthority Optional authority that can freeze token accounts
   */
  static createInitMintInstruction(
    programId: PublicKey,
    mint: PublicKey,
    decimals: number,
    mintAuthority: PublicKey,
    freezeAuthority: PublicKey | null,
  ): TransactionInstruction {
    let keys = [
      {pubkey: mint, isSigner: false, isWritable: true},
      {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
    ];
    const commandDataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      BufferLayout.u8('decimals'),
      Layout.publicKey('mintAuthority'),
      BufferLayout.u8('option'),
      Layout.publicKey('freezeAuthority'),
    ]);
    let data = Buffer.alloc(1024);
    {
      const encodeLength = commandDataLayout.encode(
        {
          instruction: 0, // InitializeMint instruction
          decimals,
          mintAuthority: pubkeyToBuffer(mintAuthority),
          option: freezeAuthority === null ? 0 : 1,
          freezeAuthority: pubkeyToBuffer(freezeAuthority || new PublicKey(0)),
        },
        data,
      );
      data = data.slice(0, encodeLength);
    }

    return new TransactionInstruction({
      keys,
      programId,
      data,
    });
  }

  /**
   * Construct an InitializeAccount instruction
   *
   * @param programId SPL Token program account
   * @param mint Token mint account
   * @param account New account
   * @param owner Owner of the new account
   */
  static createInitAccountInstruction(
    programId: PublicKey,
    mint: PublicKey,
    account: PublicKey,
    owner: PublicKey,
  ): TransactionInstruction {
    const keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: false},
      {pubkey: owner, isSigner: false, isWritable: false},
      {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
    ];
    const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 1, // InitializeAccount instruction
      },
      data,
    );

    return new TransactionInstruction({
      keys,
      programId,
      data,
    });
  }

  /**
   * Construct a Transfer instruction
   *
   * @param programId SPL Token program account
   * @param source Source account
   * @param destination Destination account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Number of tokens to transfer
   */
  static createTransferInstruction(
    programId: PublicKey,
    source: PublicKey,
    destination: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 3, // Transfer instruction
        amount: new u64(amount).toBuffer(),
      },
      data,
    );

    let keys = [
      {pubkey: source, isSigner: false, isWritable: true},
      {pubkey: destination, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: owner,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }
    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct an Approve instruction
   *
   * @param programId SPL Token program account
   * @param account Public key of the account
   * @param delegate Account authorized to perform a transfer of tokens from the source account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Maximum number of tokens the delegate may transfer
   */
  static createApproveInstruction(
    programId: PublicKey,
    account: PublicKey,
    delegate: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 4, // Approve instruction
        amount: new u64(amount).toBuffer(),
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: delegate, isSigner: false, isWritable: false},
    ];
    if (multiSigners.length === 0) {
      keys.push({pubkey: owner, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a Revoke instruction
   *
   * @param programId SPL Token program account
   * @param account Public key of the account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   */
  static createRevokeInstruction(
    programId: PublicKey,
    account: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 5, // Approve instruction
      },
      data,
    );

    let keys = [{pubkey: account, isSigner: false, isWritable: true}];
    if (multiSigners.length === 0) {
      keys.push({pubkey: owner, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a SetAuthority instruction
   *
   * @param programId SPL Token program account
   * @param account Public key of the account
   * @param newAuthority New authority of the account
   * @param authorityType Type of authority to set
   * @param currentAuthority Current authority of the specified type
   * @param multiSigners Signing accounts if `currentAuthority` is a multiSig
   */
  static createSetAuthorityInstruction(
    programId: PublicKey,
    account: PublicKey,
    newAuthority: PublicKey | null,
    authorityType: AuthorityType,
    currentAuthority: PublicKey,
    multiSigners: Array<Account>,
  ): TransactionInstruction {
    const commandDataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      BufferLayout.u8('authorityType'),
      BufferLayout.u8('option'),
      Layout.publicKey('newAuthority'),
    ]);

    let data = Buffer.alloc(1024);
    {
      const encodeLength = commandDataLayout.encode(
        {
          instruction: 6, // SetAuthority instruction
          authorityType: AuthorityTypeCodes[authorityType],
          option: newAuthority === null ? 0 : 1,
          newAuthority: pubkeyToBuffer(newAuthority || new PublicKey(0)),
        },
        data,
      );
      data = data.slice(0, encodeLength);
    }

    let keys = [{pubkey: account, isSigner: false, isWritable: true}];
    if (multiSigners.length === 0) {
      keys.push({pubkey: currentAuthority, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: currentAuthority, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a MintTo instruction
   *
   * @param programId SPL Token program account
   * @param mint Public key of the mint
   * @param dest Public key of the account to mint to
   * @param authority The mint authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Amount to mint
   */
  static createMintToInstruction(
    programId: PublicKey,
    mint: PublicKey,
    dest: PublicKey,
    authority: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 7, // MintTo instruction
        amount: new u64(amount).toBuffer(),
      },
      data,
    );

    let keys = [
      {pubkey: mint, isSigner: false, isWritable: true},
      {pubkey: dest, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: authority,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: authority, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a Burn instruction
   *
   * @param programId SPL Token program account
   * @param mint Mint for the account
   * @param account Account to burn tokens from
   * @param owner Owner of the account
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount amount to burn
   */
  static createBurnInstruction(
    programId: PublicKey,
    mint: PublicKey,
    account: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 8, // Burn instruction
        amount: new u64(amount).toBuffer(),
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: owner,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a Close instruction
   *
   * @param programId SPL Token program account
   * @param account Account to close
   * @param dest Account to receive the remaining balance of the closed account
   * @param authority Account Close authority
   * @param multiSigners Signing accounts if `owner` is a multiSig
   */
  static createCloseAccountInstruction(
    programId: PublicKey,
    account: PublicKey,
    dest: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 9, // CloseAccount instruction
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: dest, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({pubkey: owner, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a Freeze instruction
   *
   * @param programId SPL Token program account
   * @param account Account to freeze
   * @param mint Mint account
   * @param authority Mint freeze authority
   * @param multiSigners Signing accounts if `owner` is a multiSig
   */
  static createFreezeAccountInstruction(
    programId: PublicKey,
    account: PublicKey,
    mint: PublicKey,
    authority: PublicKey,
    multiSigners: Array<Account>,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 10, // FreezeAccount instruction
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: false},
    ];
    if (multiSigners.length === 0) {
      keys.push({pubkey: authority, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: authority, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a Thaw instruction
   *
   * @param programId SPL Token program account
   * @param account Account to thaw
   * @param mint Mint account
   * @param authority Mint freeze authority
   * @param multiSigners Signing accounts if `owner` is a multiSig
   */
  static createThawAccountInstruction(
    programId: PublicKey,
    account: PublicKey,
    mint: PublicKey,
    authority: PublicKey,
    multiSigners: Array<Account>,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 11, // ThawAccount instruction
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: false},
    ];
    if (multiSigners.length === 0) {
      keys.push({pubkey: authority, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: authority, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a TransferChecked instruction
   *
   * @param programId SPL Token program account
   * @param source Source account
   * @param mint Mint account
   * @param destination Destination account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Number of tokens to transfer
   * @param decimals Number of decimals in transfer amount
   */
  static createTransferCheckedInstruction(
    programId: PublicKey,
    source: PublicKey,
    mint: PublicKey,
    destination: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
      BufferLayout.u8('decimals'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 12, // TransferChecked instruction
        amount: new u64(amount).toBuffer(),
        decimals,
      },
      data,
    );

    let keys = [
      {pubkey: source, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: false},
      {pubkey: destination, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: owner,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }
    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct an ApproveChecked instruction
   *
   * @param programId SPL Token program account
   * @param account Public key of the account
   * @param mint Mint account
   * @param delegate Account authorized to perform a transfer of tokens from the source account
   * @param owner Owner of the source account
   * @param multiSigners Signing accounts if `owner` is a multiSig
   * @param amount Maximum number of tokens the delegate may transfer
   * @param decimals Number of decimals in approve amount
   */
  static createApproveCheckedInstruction(
    programId: PublicKey,
    account: PublicKey,
    mint: PublicKey,
    delegate: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
      BufferLayout.u8('decimals'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 13, // ApproveChecked instruction
        amount: new u64(amount).toBuffer(),
        decimals,
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: false},
      {pubkey: delegate, isSigner: false, isWritable: false},
    ];
    if (multiSigners.length === 0) {
      keys.push({pubkey: owner, isSigner: true, isWritable: false});
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a MintToChecked instruction
   *
   * @param programId SPL Token program account
   * @param mint Public key of the mint
   * @param dest Public key of the account to mint to
   * @param authority The mint authority
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount Amount to mint
   * @param decimals Number of decimals in amount to mint
   */
  static createMintToCheckedInstruction(
    programId: PublicKey,
    mint: PublicKey,
    dest: PublicKey,
    authority: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
      BufferLayout.u8('decimals'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 14, // MintToChecked instruction
        amount: new u64(amount).toBuffer(),
        decimals,
      },
      data,
    );

    let keys = [
      {pubkey: mint, isSigner: false, isWritable: true},
      {pubkey: dest, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: authority,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: authority, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Construct a BurnChecked instruction
   *
   * @param programId SPL Token program account
   * @param mint Mint for the account
   * @param account Account to burn tokens from
   * @param owner Owner of the account
   * @param multiSigners Signing accounts if `authority` is a multiSig
   * @param amount amount to burn
   */
  static createBurnCheckedInstruction(
    programId: PublicKey,
    mint: PublicKey,
    account: PublicKey,
    owner: PublicKey,
    multiSigners: Array<Account>,
    amount: number | u64,
    decimals: number,
  ): TransactionInstruction {
    const dataLayout = BufferLayout.struct([
      BufferLayout.u8('instruction'),
      Layout.uint64('amount'),
      BufferLayout.u8('decimals'),
    ]);

    const data = Buffer.alloc(dataLayout.span);
    dataLayout.encode(
      {
        instruction: 15, // BurnChecked instruction
        amount: new u64(amount).toBuffer(),
        decimals,
      },
      data,
    );

    let keys = [
      {pubkey: account, isSigner: false, isWritable: true},
      {pubkey: mint, isSigner: false, isWritable: true},
    ];
    if (multiSigners.length === 0) {
      keys.push({
        pubkey: owner,
        isSigner: true,
        isWritable: false,
      });
    } else {
      keys.push({pubkey: owner, isSigner: false, isWritable: false});
      multiSigners.forEach(signer =>
        keys.push({
          pubkey: signer.publicKey,
          isSigner: true,
          isWritable: false,
        }),
      );
    }

    return new TransactionInstruction({
      keys,
      programId: programId,
      data,
    });
  }

  /**
   * Get the address for the associated token account
   *
   * @param associatedProgramId SPL Associated Token program account
   * @param programId SPL Token program account
   * @param mint Token mint account
   * @param owner Owner of the new account
   * @return Public key of the associated token account
   */
  static async getAssociatedTokenAddress(
    associatedProgramId: PublicKey,
    programId: PublicKey,
    mint: PublicKey,
    owner: PublicKey,
  ): Promise<PublicKey> {
    if (!PublicKey.isOnCurve(owner.toBuffer())) {
      throw new Error(`Owner cannot sign: ${owner.toString()}`);
    }
    return (
      await PublicKey.findProgramAddress(
        [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
        associatedProgramId,
      )
    )[0];
  }

  /**
   * Construct the AssociatedTokenProgram instruction to create the associated
   * token account
   *
   * @param associatedProgramId SPL Associated Token program account
   * @param programId SPL Token program account
   * @param mint Token mint account
   * @param associatedAccount New associated account
   * @param owner Owner of the new account
   * @param payer Payer of fees
   */
  static createAssociatedTokenAccountInstruction(
    associatedProgramId: PublicKey,
    programId: PublicKey,
    mint: PublicKey,
    associatedAccount: PublicKey,
    owner: PublicKey,
    payer: PublicKey,
  ): TransactionInstruction {
    const data = Buffer.alloc(0);

    let keys = [
      {pubkey: payer, isSigner: true, isWritable: true},
      {pubkey: associatedAccount, isSigner: false, isWritable: true},
      {pubkey: owner, isSigner: false, isWritable: false},
      {pubkey: mint, isSigner: false, isWritable: false},
      {pubkey: SystemProgram.programId, isSigner: false, isWritable: false},
      {pubkey: programId, isSigner: false, isWritable: false},
      {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
    ];

    return new TransactionInstruction({
      keys,
      programId: associatedProgramId,
      data,
    });
  }
}
