import { createCreateMetadataAccountV3Instruction } from '@metaplex-solarti/mpl-token-metadata';
import { Keypair } from '@solarti/web3.js';
import { assertSft } from '../models/Sft.mjs';
import { useOperation, makeConfirmOptionsFinalizedOnMainnet } from '../../../types/Operation.mjs';
import { toPublicKey } from '../../../types/PublicKey.mjs';
import { TransactionBuilder } from '../../../utils/TransactionBuilder.mjs';
import { isSigner } from '../../../types/Signer.mjs';

// -----------------
// Operation
// -----------------

const Key = 'CreateSftOperation';

/**
 * Creates a new SFT.
 *
 * ```ts
 * const { sft } = await metaplex
 *   .nfts()
 *   .createSft({
 *     name: 'My SFT',
 *     uri: 'https://example.com/my-sft',
 *     sellerFeeBasisPoints: 250, // 2.5%
 *   };
 * ```
 *
 * @group Operations
 * @category Constructors
 */
const createSftOperation = useOperation(Key);

/**
 * @group Operations
 * @category Types
 */

/**
 * @group Operations
 * @category Handlers
 */
const createSftOperationHandler = {
  handle: async (operation, metaplex, scope) => {
    const {
      useNewMint = Keypair.generate(),
      useExistingMint,
      tokenOwner,
      tokenAddress: tokenSigner
    } = operation.input;
    const mintAddress = useExistingMint ?? useNewMint.publicKey;
    const associatedTokenAddress = tokenOwner ? metaplex.tokens().pdas().associatedTokenAccount({
      mint: mintAddress,
      owner: tokenOwner,
      programs: scope.programs
    }) : null;
    const tokenAddress = tokenSigner ? toPublicKey(tokenSigner) : associatedTokenAddress;
    let tokenExists;
    if (!!useExistingMint && !!tokenAddress) {
      const tokenAccount = await metaplex.rpc().getAccount(tokenAddress);
      tokenExists = tokenAccount.exists;
    } else {
      tokenExists = false;
    }
    const builder = await createSftBuilder(metaplex, {
      ...operation.input,
      useNewMint,
      tokenExists
    }, scope);
    scope.throwIfCanceled();
    const confirmOptions = makeConfirmOptionsFinalizedOnMainnet(metaplex, scope.confirmOptions);
    const output = await builder.sendAndConfirm(metaplex, confirmOptions);
    scope.throwIfCanceled();
    const sft = await metaplex.nfts().findByMint({
      mintAddress: output.mintAddress,
      tokenAddress: output.tokenAddress ?? undefined
    }, scope);
    scope.throwIfCanceled();
    assertSft(sft);
    return {
      ...output,
      sft
    };
  }
};

// -----------------
// Builder
// -----------------

/**
 * @group Transaction Builders
 * @category Inputs
 */

/**
 * Creates a new SFT.
 *
 * ```ts
 * const transactionBuilder = await metaplex
 *   .nfts()
 *   .builders()
 *   .createSft({
 *     name: 'My SFT',
 *     uri: 'https://example.com/my-sft',
 *     sellerFeeBasisPoints: 250, // 2.5%
 *   });
 * ```
 *
 * @group Transaction Builders
 * @category Constructors
 */
const createSftBuilder = async (metaplex, params, options = {}) => {
  const {
    programs,
    payer = metaplex.rpc().getDefaultFeePayer()
  } = options;
  const {
    useNewMint = Keypair.generate(),
    updateAuthority = metaplex.identity(),
    mintAuthority = metaplex.identity()
  } = params;
  const mintAndTokenBuilder = await createMintAndTokenForSftBuilder(metaplex, params, {
    programs,
    payer
  }, useNewMint);
  const {
    mintAddress,
    tokenAddress
  } = mintAndTokenBuilder.getContext();
  const tokenMetadataProgram = metaplex.programs().getTokenMetadata(programs);
  const metadataPda = metaplex.nfts().pdas().metadata({
    mint: mintAddress,
    programs
  });
  const creatorsInput = params.creators ?? [{
    address: updateAuthority.publicKey,
    authority: updateAuthority,
    share: 100
  }];
  const creators = creatorsInput.length > 0 ? creatorsInput.map(creator => ({
    ...creator,
    verified: creator.address.equals(updateAuthority.publicKey)
  })) : null;
  const createMetadataInstruction = createCreateMetadataAccountV3Instruction({
    metadata: metadataPda,
    mint: mintAddress,
    mintAuthority: mintAuthority.publicKey,
    payer: payer.publicKey,
    updateAuthority: updateAuthority.publicKey
  }, {
    createMetadataAccountArgsV3: {
      data: {
        name: params.name,
        symbol: params.symbol ?? '',
        uri: params.uri,
        sellerFeeBasisPoints: params.sellerFeeBasisPoints,
        creators,
        collection: params.collection ? {
          key: params.collection,
          verified: false
        } : null,
        uses: params.uses ?? null
      },
      isMutable: params.isMutable ?? true,
      collectionDetails: params.isCollection ? {
        __kind: 'V1',
        size: 0
      } // Program will hardcode size to zero anyway.
      : null
    }
  }, tokenMetadataProgram.address);

  // When the payer is different than the update authority, the latter will
  // not be marked as a signer and therefore signing as a creator will fail.
  createMetadataInstruction.keys[4].isSigner = true;
  const verifyAdditionalCreatorInstructions = creatorsInput.filter(creator => {
    return !!creator.authority && !creator.address.equals(updateAuthority.publicKey);
  }).map(creator => {
    return metaplex.nfts().builders().verifyCreator({
      mintAddress,
      creator: creator.authority
    }, {
      programs,
      payer
    });
  });
  return TransactionBuilder.make().setFeePayer(payer).setContext({
    mintAddress,
    metadataAddress: metadataPda,
    tokenAddress
  })

  // Create the mint and token accounts before minting 1 token to the owner.
  .add(mintAndTokenBuilder)

  // Create metadata account.
  .add({
    instruction: createMetadataInstruction,
    signers: [payer, mintAuthority, updateAuthority],
    key: params.createMetadataInstructionKey ?? 'createMetadata'
  })

  // Verify additional creators.
  .add(...verifyAdditionalCreatorInstructions)

  // Verify collection.
  .when(!!params.collection && !!params.collectionAuthority, builder => builder.add(metaplex.nfts().builders().verifyCollection({
    mintAddress,
    collectionMintAddress: params.collection,
    collectionAuthority: params.collectionAuthority,
    isDelegated: params.collectionAuthorityIsDelegated ?? false,
    isSizedCollection: params.collectionIsSized ?? true
  }, {
    payer,
    programs
  })));
};
const createMintAndTokenForSftBuilder = async (metaplex, params, options, useNewMint) => {
  const {
    programs,
    payer = metaplex.rpc().getDefaultFeePayer()
  } = options;
  const {
    mintAuthority = metaplex.identity(),
    freezeAuthority = metaplex.identity().publicKey,
    tokenExists = false
  } = params;
  const mintAddress = params.useExistingMint ?? useNewMint.publicKey;
  const associatedTokenAddress = params.tokenOwner ? metaplex.tokens().pdas().associatedTokenAccount({
    mint: mintAddress,
    owner: params.tokenOwner,
    programs
  }) : null;
  const tokenAddress = params.tokenAddress ? toPublicKey(params.tokenAddress) : associatedTokenAddress;
  const builder = TransactionBuilder.make().setFeePayer(payer).setContext({
    mintAddress,
    tokenAddress
  });

  // Create the mint account if it doesn't exist.
  if (!params.useExistingMint) {
    builder.add(await metaplex.tokens().builders().createMint({
      decimals: params.decimals ?? 0,
      mint: useNewMint,
      mintAuthority: mintAuthority.publicKey,
      freezeAuthority,
      createAccountInstructionKey: params.createMintAccountInstructionKey,
      initializeMintInstructionKey: params.initializeMintInstructionKey
    }, {
      programs,
      payer
    }));
  }

  // Create the token account if it doesn't exist.
  const isNewToken = !!params.tokenAddress && isSigner(params.tokenAddress);
  const isNewAssociatedToken = !!params.tokenOwner;
  if (!tokenExists && (isNewToken || isNewAssociatedToken)) {
    builder.add(await metaplex.tokens().builders().createToken({
      mint: mintAddress,
      owner: params.tokenOwner,
      token: params.tokenAddress,
      createAssociatedTokenAccountInstructionKey: params.createAssociatedTokenAccountInstructionKey,
      createAccountInstructionKey: params.createTokenAccountInstructionKey,
      initializeTokenInstructionKey: params.initializeTokenInstructionKey
    }, {
      programs,
      payer
    }));
  }

  // Mint provided amount to the token account.
  if (tokenAddress && params.tokenAmount) {
    builder.add(await metaplex.tokens().builders().mint({
      mintAddress,
      toToken: tokenAddress,
      toTokenExists: true,
      amount: params.tokenAmount,
      mintAuthority,
      mintTokensInstructionKey: params.mintTokensInstructionKey
    }, {
      programs,
      payer
    }));
  }
  return builder;
};

export { createSftBuilder, createSftOperation, createSftOperationHandler };
//# sourceMappingURL=createSft.mjs.map
