Skip to content

Commit

Permalink
fix: fixed minting compressed NFTs (#511)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickfrosty authored May 9, 2023
1 parent 37add4e commit bcd01a1
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-pugs-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/js': patch
---

Fix minting compressed NFTs
11 changes: 10 additions & 1 deletion packages/js/src/plugins/nftModule/NftClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
approveNftDelegateOperation,
ApproveNftUseAuthorityInput,
approveNftUseAuthorityOperation,
CreateCompressedNftInput,
createCompressedNftOperation,
CreateNftInput,
createNftOperation,
CreateSftInput,
Expand Down Expand Up @@ -229,7 +231,7 @@ export class NftClient {
model: T,
input?: Omit<
FindNftByMintInput,
'mintAddress' | 'tokenAddres' | 'tokenOwner'
'mintAddress' | 'tokenAddress' | 'tokenOwner'
>,
options?: OperationOptions
): Promise<T extends Metadata | PublicKey ? Nft | Sft : T> {
Expand All @@ -249,6 +251,13 @@ export class NftClient {

/** {@inheritDoc createNftOperation} */
create(input: CreateNftInput, options?: OperationOptions) {
if (input?.tree)
return this.metaplex
.operations()
.execute(
createCompressedNftOperation(input as CreateCompressedNftInput),
options
);
return this.metaplex
.operations()
.execute(createNftOperation(input), options);
Expand Down
99 changes: 78 additions & 21 deletions packages/js/src/plugins/nftModule/operations/createCompressedNft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
changeLogEventV1Beet,
deserializeChangeLogEventV1,
} from '@solana/spl-account-compression';
import { PublicKey } from '@solana/web3.js';
import { BN } from 'bn.js';
import base58 from 'bs58';
import { SendAndConfirmTransactionResponse } from '../../rpcModule';
import { assertNft, Nft } from '../models';
import { Option, TransactionBuilder, TransactionBuilderOptions } from '@/utils';
Expand Down Expand Up @@ -194,11 +195,20 @@ export type CreateCompressedNftOutput = {
/** The blockchain response from sending and confirming the transaction. */
response: SendAndConfirmTransactionResponse;

/** The newly created SFT and, potentially, its associated token. */
/** The newly created NFT and, potentially, its associated token. */
nft: Nft;

/** The asset id of the leaf. */
/** The mint address is the compressed NFT's assetId. */
mintAddress: PublicKey;

/** The metadata address is the compressed NFT's assetId. */
metadataAddress: PublicKey;

/** The master edition address is the compressed NFT's assetId. */
masterEditionAddress: PublicKey;

/** The token address is the compressed NFT's assetId. */
tokenAddress: PublicKey;
};

/**
Expand Down Expand Up @@ -226,29 +236,64 @@ export const createCompressedNftOperationHandler: OperationHandler<CreateCompres
const output = await builder.sendAndConfirm(metaplex, confirmOptions);
scope.throwIfCanceled();

const {
response: { signature },
} = output;
const txInfo = await metaplex.connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
const txInfo = await metaplex.connection.getTransaction(
output.response.signature,
{
maxSupportedTransactionVersion: 0,
}
);
scope.throwIfCanceled();

// find the index of the bubblegum instruction
const relevantIndex =
txInfo!.transaction.message.compiledInstructions.findIndex(
(instruction) => {
return (
txInfo?.transaction.message.staticAccountKeys[
instruction.programIdIndex
].toBase58() === 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY'
);
}
);

// locate the no-op inner instructions called via cpi from bubblegum
const relevantInnerIxs = txInfo!.meta?.innerInstructions?.[
relevantIndex
].instructions.filter((instruction) => {
return (
txInfo?.transaction.message.staticAccountKeys[
instruction.programIdIndex
].toBase58() === 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'
);
});
const relevantIx = txInfo!.transaction.message.compiledInstructions.find(
(instruction) => {
return (
txInfo!.transaction.message.staticAccountKeys[
instruction.programIdIndex
].toBase58() === 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'

// when no valid noop instructions are found, throw an error
if (!relevantInnerIxs || relevantInnerIxs.length == 0)
throw Error('Unable to locate valid noop instructions');

// locate the asset index by attempting to locate and parse the correct `relevantInnerIx`
let assetIndex: number | undefined = undefined;
// note: the `assetIndex` is expected to be at position `1`, and normally expect only 2 `relevantInnerIx`
for (let i = relevantInnerIxs.length - 1; i > 0; i--) {
try {
const changeLogEvent = deserializeChangeLogEventV1(
Buffer.from(base58.decode(relevantInnerIxs[i]?.data!))
);

// extract a successful changelog index
assetIndex = changeLogEvent?.index;
} catch (__) {
// do nothing, invalid data is handled just after the for loop
}
);
}

const [changeLog] = changeLogEventV1Beet.deserialize(
Buffer.from(relevantIx!.data)
);
// when no `assetIndex` was found, throw an error
if (typeof assetIndex == 'undefined')
throw Error('Unable to locate the newly minted assetId ');

const assetId = await getLeafAssetId(
operation.input.tree,
new BN(changeLog.index)
new BN(assetIndex)
);

const nft = await metaplex.nfts().findByAssetId(
Expand All @@ -260,7 +305,19 @@ export const createCompressedNftOperationHandler: OperationHandler<CreateCompres
scope.throwIfCanceled();

assertNft(nft);
return { ...output, nft };

return {
...output,
nft,
/**
* the assetId is impossible to know before the compressed nft is minted
* all these addresses are derived from, or are, the `assetId`
*/
mintAddress: assetId,
tokenAddress: assetId,
metadataAddress: nft.metadataAddress,
masterEditionAddress: nft.edition.address,
};
},
};

Expand Down Expand Up @@ -316,7 +373,7 @@ export type CreateCompressedNftBuilderContext = Omit<
>;

/**
* Creates a new SFT.
* Creates a new compressed NFT.
*
* ```ts
* const transactionBuilder = await metaplex
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/plugins/nftModule/operations/createNft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export type CreateNftInput = {
* Describes the asset class of the token.
* It can be one of the following:
* - `TokenStandard.NonFungible`: A traditional NFT (master edition).
* - `TokenStandard.FungibleAsset`: A fungible token with metadata that can also have attrributes.
* - `TokenStandard.FungibleAsset`: A fungible token with metadata that can also have attributes.
* - `TokenStandard.Fungible`: A fungible token with simple metadata.
* - `TokenStandard.NonFungibleEdition`: A limited edition NFT "printed" from a master edition.
* - `TokenStandard.ProgrammableNonFungible`: A master edition NFT with programmable configuration.
Expand Down
3 changes: 2 additions & 1 deletion packages/js/test/helpers/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
CreateNftInput,
KeypairSigner,
CreateSftInput,
NftWithToken,
} from '@/index';

export type MetaplexTestOptions = {
Expand Down Expand Up @@ -57,7 +58,7 @@ export const createNft = async (
...input,
});

return nft;
return nft as NftWithToken;
};

export const createCollectionNft = (
Expand Down

0 comments on commit bcd01a1

Please sign in to comment.