Git Product home page Git Product logo

js's Introduction

Metaplex JavaScript SDK

🚨 Deprecated library

Please note that this library is no longer actively maintained. To integrate Metaplex's products with your JavaScript applications please use our various Umi libraries instead:


This SDK helps developers get started with the on-chain tools provided by Metaplex. It focuses its API on common use-cases to provide a smooth developer experience whilst allowing third parties to extend its features via plugins.

Please note that this SDK has been re-implemented from scratch and is still in active development. This means some of the core API and interfaces might change from one version to another. However, feel free to use it and provide some early feedback if you wish to contribute to the direction of this project.

Installation

npm install @metaplex-foundation/js @solana/web3.js

πŸ”₯ Pro Tip: Check out our examples and starter kits on the "JS Examples" repository.

Setup

The entry point to the JavaScript SDK is a Metaplex instance that will give you access to its API.

It accepts a Connection instance from @solana/web3.js that will be used to communicate with the cluster.

import { Metaplex } from "@metaplex-foundation/js";
import { Connection, clusterApiUrl } from "@solana/web3.js";

const connection = new Connection(clusterApiUrl("mainnet-beta"));
const metaplex = new Metaplex(connection);

On top of that, you can customise who the SDK should interact on behalf of and which storage provider to use when uploading assets. We refer to these as "Identity Drivers" and "Storage Drivers" respectively. You may change these drivers by calling the use method on the Metaplex instance like so. We'll see all available drivers in more detail below.

import { Metaplex, keypairIdentity, bundlrStorage } from "@metaplex-foundation/js";
import { Connection, clusterApiUrl, Keypair } from "@solana/web3.js";

const connection = new Connection(clusterApiUrl("mainnet-beta"));
const wallet = Keypair.generate();

const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(wallet))
    .use(bundlrStorage());

Notice how you can create a Metaplex instance using Metaplex.make(...) instead of new Metaplex(...) in order to make the fluent API more readable.

Usage

Once properly configured, that Metaplex instance can be used to access modules providing different sets of features. Currently, there is only one documented NFT module that can be accessed via the nfts() method. From that module, you will be able to find, create and update NFTs with more features to come.

For instance, here is how you can fetch an NFT by its mint address.

const nft = await metaplex.nfts().findByMint({ mintAddress });

We call findByMint an Operation on the NFT Module. Each operation accepts an input object as its first argument that is defined by the operation itself. Additionally, each operation accepts a second optional argument that is shared by all operations and used for more generic options. For instance, you may pass an AbortSignal to this second argument to cancel the operation before it finishes β€” similarly to how you would cancel an HTTP request.

// Create an AbortController that aborts in 100ms.
const abortController = new AbortController();
setTimeout(() => abortController.abort(), 100);

// Pass the AbortController's signal to the operation.
const nft = await metaplex.nfts().findByMint({ mintAddress }, {
    signal: abortController.signal
});

Now, let’s look into the NFT module in a bit more detail before moving on to the identity and storage drivers.

NFTs

The NFT module can be accessed via metaplex.nfts() and provides the following methods.

And the following model, either returned or used by the above methods.

You may also be interested in browsing the API References of that module.

findByMint

The findByMint method accepts a mintAddress public key and returns an Nft object.

const mintAddress = new PublicKey("ATe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeL");

const nft = await metaplex.nfts().findByMint({ mintAddress });

The returned Nft object will have its JSON metadata already loaded so you can, for instance, access its image URL like so (provided it is present in the downloaded metadata).

const imageUrl = nft.json.image;

Similarly, the Edition information of the NFT β€” original or printed β€” is also available on the object via the edition property. Its type depends on whether the NFT is the original or a printed edition.

const editionAddress = nft.edition.address;

if (nft.edition.isOriginal) {
    const totalPrintedNfts = nft.edition.supply;
    const maxNftsThatCanBePrinted = nft.edition.maxSupply;
} else {
    const mintAddressOfOriginalNft = nft.edition.parent;
    const editionNumber = nft.edition.number;
}

You can read more about the NFT model below.

findAllByMintList

The findAllByMintList operation accepts an array of mint addresses and returns an array of NFTs. However, null values will be returned for each provided mint address that is not associated with an NFT.

Note that this is much more efficient than calling findByMint for each mint in the list as the SDK can optimise the query and fetch multiple NFTs in much fewer requests.

const [nftA, nftB] = await metaplex.nfts().findAllByMintList({
    mints: [mintA, mintB]
});

NFTs retrieved via findAllByMintList may be of type Metadata rather than Nft.

What this means is they won't have their JSON metadata loaded because this would require one request per NFT and could be inefficient if you provide a long list of mint addresses. Additionally, you might want to fetch these on-demand, as the NFTs are being displayed on your web app for instance. The same goes for the edition property which requires an extra account to fetch and might be irrelevant until the user clicks on the NFT.

Note that, since plugins can swap operation handlers with their own implementations, it is possible that a plugin relying on indexers return an array of Nfts directly instead of Metadatas. The default implementation though, will return Metadatas.

Thus, if you want to load the json and/or edition properties of an NFT, you need to load that Metadata into an Nft. Which you can do with the next operation.

load

For performance reasons, when fetching NFTs in bulk, you may receive Metadatas which exclude the JSON Metadata and the Edition information of the NFT. In order to transform a Metadata into an Nft, you may use the load operation like so.

const nft = await metaplex.nfts().load({ metadata });

This will give you access to the json and edition properties of the NFT as explained in the NFT model documentation.

findAllByOwner

The findAllByOwner method accepts a public key and returns all NFTs owned by that public key.

const myNfts = await metaplex.nfts().findAllByOwner({
    owner: metaplex.identity().publicKey
});

Similarly to findAllByMintList, the returned NFTs may be Metadatas.

findAllByCreator

The findAllByCreator method accepts a public key and returns all NFTs that have that public key registered as their first creator. Additionally, you may provide an optional position parameter to match the public key at a specific position in the creator list.

const nfts = await metaplex.nfts().findAllByCreator({ creator });
const nfts = await metaplex.nfts().findAllByCreator({ creator, position: 1 }); // Equivalent to the previous line.
const nfts = await metaplex.nfts().findAllByCreator({ creator, position: 2 }); // Now matching the second creator field.

Similarly to findAllByMintList, the returned NFTs may be Metadatas.

uploadMetadata

When creating or updating an NFT, you will need a URI pointing to some JSON Metadata describing the NFT. Depending on your requirement, you may do this on-chain or off-chain.

If your JSON metadata is not already uploaded, you may do this using the SDK via the uploadMetadata method. It accepts a metadata object and returns the URI of the uploaded metadata. Where exactly the metadata will be uploaded depends on the selected StorageDriver.

const { uri } = await metaplex.nfts().uploadMetadata({
    name: "My NFT",
    description: "My description",
    image: "https://arweave.net/123",
});

console.log(uri) // https://arweave.net/789

Some properties inside that metadata object will also require you to upload some assets to provide their URI β€” such as the image property on the example above.

To make this process easier, the uploadMetadata method will recognise any instances of MetaplexFile within the provided object and upload them in bulk to the current storage driver. It will then create a new version of the provided metadata where all instances of MetaplexFile are replaced with their URI. Finally, it will upload that replaced metadata to the storage driver and return it.

// Assuming the user uploaded two assets via an input field of type "file".
const browserFiles = event.target.files;

const { uri, metadata } = await metaplex.nfts().uploadMetadata({
    name: "My NFT",
    image: await toMetaplexFileFromBrowser(browserFiles[0]),
    properties: {
        files: [
            {
                type: "video/mp4",
                uri: await toMetaplexFileFromBrowser(browserFiles[1]),
            },
        ]
    }
});

console.log(metadata.image) // https://arweave.net/123
console.log(metadata.properties.files[0].uri) // https://arweave.net/456
console.log(uri) // https://arweave.net/789

Note that MetaplexFiles can be created in various different ways based on where the file is coming from. You can read more about MetaplexFile objects and how to use them here.

create

The create method accepts a variety of parameters that define the on-chain data of the NFT. The only parameters required are its name, its sellerFeeBasisPoints β€” i.e. royalties β€” and the uri pointing to its JSON metadata β€” remember that you can use uploadMetadata to get that URI. All other parameters are optional as the SDK will do its best to provide sensible default values.

Here's how you can create a new NFT with minimum configuration.

const { nft } = await metaplex.nfts().create({
    uri: "https://arweave.net/123",
    name: "My NFT",
    sellerFeeBasisPoints: 500, // Represents 5.00%.
});

This will take care of creating the mint account, the associated token account, the metadata PDA and the original edition PDA (a.k.a. the master edition) for you.

Additionally, since no other optional parameters were provided, it will do its best to provide sensible default values for the rest of the parameters. Namely:

  • Since no owner, mint authority or update authority were provided, the β€œidentity” of the SDK will be used by default for these parameters. Meaning the SDK's identity will be the owner of that new NFT.
  • It will also default to setting the identity as the first and only creator with a 100% share.
  • It will default to making the NFT mutable β€” meaning the update authority will be able to update it later on.

If some of these default parameters are not suitable for your use case, you may provide them explicitly when creating the NFT. Here is the exhaustive list of parameters accepted by the create method.

update

The update method accepts an Nft object and a set of parameters to update on the NFT.

For instance, here is how you would change the on-chain name of an NFT.

await metaplex.nfts().update({ 
    nftOrSft: nft,
    name: "My Updated Name"
});

Anything that you don’t provide in the parameters will stay unchanged. Note that it will not fetch the updated NFT in order to avoid the extra HTTP call if you don't need it. If you do need to refresh the NFT instance to access the latest data, you may do that using the refresh operation.

const updatedNft = await metaplex.nfts().refresh(nft);

If you’d like to change the JSON metadata of the NFT, you’d first need to upload a new metadata object using the uploadMetadata method and then use the provided URI to update the NFT.

const { uri: newUri } = await metaplex.nfts().uploadMetadata({
    ...nft.json,
    name: "My Updated Metadata Name",
    description: "My Updated Metadata Description",
});

await metaplex.nfts().update({ 
    nftOrSft: nft,
    uri: newUri
});

printNewEdition

The printNewEdition method requires the mint address of the original NFT and returns a brand-new NFT printed from the original edition.

This is how you would print a new edition of the originalNft NFT.

const { nft: printedNft } = await metaplex.nfts().printNewEdition({
    originalMint: originalNft.mint
});

By default, it will print using the token account of the original NFT as proof of ownership, and it will do so using the current identity of the SDK. You may customise all of these parameters by providing them explicitly.

await metaplex.nfts().printNewEdition({
    originalMint,
    newMint,                   // Defaults to a brand-new Keypair.
    newUpdateAuthority,        // Defaults to the current identity.
    newOwner,                  // Defaults to the current identity.
    originalTokenAccountOwner, // Defaults to the current identity.
    originalTokenAccount,      // Defaults to the associated token account of the current identity.
});

Notice that, by default, update authority will be transferred to the metaplex identity. If you want the printed edition to retain the update authority of the original edition, you might want to provide it explicitly like so.

await metaplex.nfts().printNewEdition({
    originalMint,
    newUpdateAuthority: originalNft.updateAuthorityAddress,
});

useNft

The use method requires a usable NFT and will decrease the amount of uses by one. You may also provide the numberOfUses parameter, if you'd like to use it more than once in the same instruction.

await mx.nfts().use({ mintAddress: nft.address }); // Use once.
await mx.nfts().use({ mintAddress: nft.address, numberOfUses: 3 }); // Use three times.

The Nft model

All of the methods above either return or interact with an Nft object. The Nft object is a read-only data representation of your NFT that contains all the information you need at the top level.

Here is an overview of the properties that are available on the Nft object.

type Nft = Readonly<{
    model: 'nft';
    address: PublicKey;
    metadataAddress: Pda;
    updateAuthorityAddress: PublicKey;
    json: Option<Json>;
    jsonLoaded: boolean;
    name: string;
    symbol: string;
    uri: string;
    isMutable: boolean;
    primarySaleHappened: boolean;
    sellerFeeBasisPoints: number;
    editionNonce: Option<number>;
    creators: Creator[];
    tokenStandard: Option<TokenStandard>;
    collection: Option<{
        address: PublicKey;
        verified: boolean;
    }>;
    collectionDetails: Option<{
        version: 'V1';
        size: BigNumber;
    }>;
    uses: Option<{
        useMethod: UseMethod;
        remaining: BigNumber;
        total: BigNumber;
    }>;
    mint: {
        model: 'mint';
        address: PublicKey;
        mintAuthorityAddress: Option<PublicKey>;
        freezeAuthorityAddress: Option<PublicKey>;
        decimals: number;
        supply: SplTokenAmount;
        isWrappedSol: boolean;
        currency: SplTokenCurrency;
    };
    edition:
        | {
            model: 'nftEdition';
            isOriginal: true;
            address: PublicKey;
            supply: BigNumber;
            maxSupply: Option<BigNumber>;
        }
        | {
            model: 'nftEdition';
            isOriginal: false;
            address: PublicKey;
            parent: PublicKey;
            number: BigNumber;
        };
}>

Additionally, the SDK may sometimes return a Metadata instead of an Nft object. The Metadata model contains the same data as the Nft model but it excludes the following properties: json, mint and edition. This is because they are not always needed and/or can be expensive to load. Therefore, the SDK uses the following rule of thumb:

  • If you're only fetching one NFT β€” e.g. by using findByMint β€” then you will receive an Nft object containing these properties.
  • If you're fetching multiple NFTs β€” e.g. by using findAllByMintLint β€” then you will receive an array of Metadata that do not contain these properties.

You may obtain an Nft object from a Metadata object by using the load method explained above,

Candy Machines

The Candy Machine module can be accessed via metaplex.candyMachinesV2() and provides the following documented methods.

The Candy Machine actually contains more features and models but we are still in the process of documenting them.

findMintedNfts

The findMintedNfts method accepts the public key of a Candy Machine and returns all NFTs that have been minted from that Candy Machine so far.

By default, it will assume you're providing the public key of a Candy Machine v2. If you want to use a different version, you can provide the version as the second parameter.

const nfts = await metaplex.candyMachinesV2().findMintedNfts({ candyMachine });
const nfts = await metaplex.candyMachinesV2().findMintedNfts({ candyMachine, version: 2 }); // Equivalent to the previous line.
const nfts = await metaplex.candyMachinesV2().findMintedNfts({ candyMachine, version: 1 }); // Now finding NFTs for Candy Machine v1.

Note that the current implementation of this method delegates to nfts().findAllByCreator() whilst fetching the appropriate PDA for Candy Machines v2.

Similarly to findAllByMintList, the returned NFTs may be Metadatas.

Identity

The current identity of a Metaplex instance can be accessed via metaplex.identity() and provide information on the wallet we are acting on behalf of when interacting with the SDK.

This method returns an identity client with the following interface.

class IdentityClient {
    driver(): IdentityDriver;
    setDriver(newDriver: IdentityDriver): void;
    publicKey: PublicKey;
    secretKey?: Uint8Array;
    signMessage(message: Uint8Array): Promise<Uint8Array>;
    verifyMessage(message: Uint8Array, signature: Uint8Array): boolean;
    signTransaction(transaction: Transaction): Promise<Transaction>;
    signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>;
    equals(that: Signer | PublicKey): boolean;
    hasSecretKey(): this is KeypairSigner;
}

The IdentityClient delegates to whichever IdentityDriver is currently set to provide this set of methods. Thus, the implementation of these methods depends on the concrete identity driver being used. For instance, in the CLI, these methods will directly use a key pair whereas, in the browser, they will delegate to a wallet adapter.

Let’s have a quick look at the concrete identity drivers available to us.

guestIdentity

The guestIdentity driver is the default driver and requires no parameter. It is essentially a null driver that can be useful when we don’t need to send any signed transactions.

import { guestIdentity } from "@metaplex-foundation/js";

metaplex.use(guestIdentity());

If we try to sign a message or a transaction using this driver, an error will be thrown.

keypairIdentity

The keypairIdentity driver accepts a Keypair object as a parameter. This is useful when using the SDK locally such as within CLI applications.

import { keypairIdentity } from "@metaplex-foundation/js";
import { Keypair } from "@solana/web3.js";

// Load a local keypair.
const keypairFile = fs.readFileSync('/Users/username/.config/solana/id.json');
const keypair = Keypair.fromSecretKey(Buffer.from(JSON.parse(keypairFile.toString())));

// Use it in the SDK.
metaplex.use(keypairIdentity(keypair));

walletAdapterIdentity

The walletAdapterIdentity driver accepts a wallet adapter as defined by the β€œwallet-adapter” repo from Solana Labs. This is useful when using the SDK in a web application that requires the user to manually approve transactions.

import { walletAdapterIdentity } from "@metaplex-foundation/js";
import { useWallet } from '@solana/wallet-adapter-react';

const wallet = useWallet();
metaplex.use(walletAdapterIdentity(wallet));

Storage

You may access the storage client using metaplex.storage() which will give you access to the following interface.

class StorageClient {
    driver(): StorageDriver
    setDriver(newDriver: StorageDriver): void;
    getUploadPriceForBytes(bytes: number): Promise<Amount>;
    getUploadPriceForFile(file: MetaplexFile): Promise<Amount>;
    getUploadPriceForFiles(files: MetaplexFile[]): Promise<Amount>;
    upload(file: MetaplexFile): Promise<string>;
    uploadAll(files: MetaplexFile[]): Promise<string[]>;
    uploadJson<T extends object = object>(json: T): Promise<string>;
    download(uri: string, options?: RequestInit): Promise<MetaplexFile>;
    downloadJson<T extends object = object>(uri: string, options?: RequestInit): Promise<T>;
}

Similarly to the IdentityClient, the StorageClient delegates to the current StorageDriver when executing these methods. We'll take a look at the storage drivers available to us, but first, let's talk about the MetaplexFile type which is being used throughout the StorageClient API.

MetaplexFile

The MetaplexFile type is a simple wrapper around Buffer that adds additional context relevant to files and assets such as their filename, content type, extension, etc. It contains the following data.

type MetaplexFile = Readonly<{
    buffer: Buffer;
    fileName: string;
    displayName: string;
    uniqueName: string;
    contentType: string | null;
    extension: string | null;
    tags: MetaplexFileTag[];
}>

You may use the toMetaplexFile function to create a MetaplexFile object from a Buffer instance (or content string) and a filename. The filename is necessary to infer the extension and the mime type of the provided file.

const file = toMetaplexFile('The content of my file', 'my-file.txt');

You may also explicitly provide these options by passing a third parameter to the constructor.

const file = toMetaplexFile('The content of my file', 'my-file.txt', {
    displayName = 'A Nice Title For My File'; // Defaults to the filename.
    uniqueName = 'my-company/files/some-identifier'; // Defaults to a random string.
    contentType = 'text/plain'; // Infer it from filename by default.
    extension = 'txt'; // Infer it from filename by default.
    tags = [{ name: 'my-tag', value: 'some-value' }]; // Defaults to [].
});

Note that if you want to create a MetaplexFile directly from a JSON object, there's a toMetaplexFileFromJson helper method that you can use like so.

const file = toMetaplexFileFromJson({ foo: 42 });

In practice, you will most likely be creating MetaplexFiles from files either present on your computer or uploaded by some user on the browser. You can do the former by using fs.readFileSync.

const buffer = fs.readFileSync('/path/to/my-file.txt');
const file = toMetaplexFile(buffer, 'my-file.txt');

And the latter by using the toMetaplexFileFromBrowser helper method which accepts a File object as defined in the browser.

const browserFile: File = event.target.files[0];
const file: MetaplexFile = await toMetaplexFileFromBrowser(browserFile);

Okay, now let’s talk about the concrete storage drivers available to us and how to set them up.

bundlrStorage

The bundlrStorage driver is the default driver and uploads assets on Arweave using the Bundlr network.

By default, it will use the same RPC endpoint used by the Metaplex instance as a providerUrl and the mainnet address "https://node1.bundlr.network" as the Bundlr address.

You may customise these by passing a parameter object to the bundlrStorage method. For instance, here’s how you can use Bundlr on devnet.

import { bundlrStorage } from "@metaplex-foundation/js";

metaplex.use(bundlrStorage({
    address: 'https://devnet.bundlr.network',
    providerUrl: 'https://api.devnet.solana.com',
    timeout: 60000,
}));

To fund your bundlr storage account you can cast it in TypeScript like so:

const bundlrStorage = metaplex.storage().driver() as BundlrStorageDriver;

This gives you access to useful public methods such as:

bundlrStorage.fund([metaplexFile1, metaplexFile2]); // Fund using file size.
bundlrStorage.fund(1000); // Fund using byte size.
(await bundlrStorage.bundlr()).fund(1000); // Fund using lamports directly.

mockStorage

The mockStorage driver is a fake driver mostly used for testing purposes. It will not actually upload the assets anywhere but instead will generate random URLs and keep track of their content in a local dictionary. That way, once uploaded, an asset can be retrieved using the download method.

import { mockStorage } from "@metaplex-foundation/js";

metaplex.use(mockStorage());

Additional Storage Drivers

The following storage drivers are available as separate packages and must be installed separately.

Programmable NFTs

Starting from version 0.18.0, you can now create and maintain programmable NFTs via the JS SDK. Here are some quick examples using the latest instructions from Token Metadata which can be used for all token standards (not only programmable NFTs).

Note that managing rulesets is not yet supported on the JS SDK and you will need to use the Token Auth Rules library for that purpose.

Create

Create all the required accounts of an NFT. Namely, the mint account (if it doesn't already exist), the metadata account and the master edition account. Setting the tokenStandard to ProgrammableNonFungible in the example below is what makes the created NFT a programmable one. You may also provide a ruleSet account at this point.

Note that createSft can be used for fungible standards.

await metaplex.nfts().createNft({
    tokenStandard: TokenStandard.ProgrammableNonFungible,
    // ...
});

Mint

Mint new tokens. From 0 to 1 for NFTs or any for SFTs.

This will create the token account if it doesn't already exist.

await metaplex.nfts().mint({
    nftOrSft: sft,
    toOwner,
    amount: token(1),
});

Update

Update the metadata and/or master edition accounts of an asset. You may also update the ruleSet account for programmable NFTs.

await metaplex.nfts().update({
    nftOrSft,
    name: "My new NFT name",
    ruleSet: ruleSet.publicKey,
});

Transfer

Transfer an asset fully or partially (for SFTs). For programmable NFTs, it will ensure that the transfer is allowed by the ruleset. For the other token standards, it will delegate the transfer to the SPL token program.

await metaplex.nfts().transfer({
    nftOrSft,
    authority: ownerA,
    fromOwner: ownerA.publicKey,
    toOwner: ownerB.publicKey,
    amount: token(1),
});

Delegate

Approves a new delegate authority for a given role. There are two types of delegates: metadata delegates and token delegates.

  • Metadata delegates are approved by the update authority of the NFT and each manages one aspect of the metadata account. There can be multiple metadata delegates for the same asset.
  • Token delegates are approved by the owner of an NFT and are used to transfer, lock and/or burn tokens. There can be only one token delegate per token account.

You can read more about delegates and their roles in the Programmable NFT Guide.

// Metadata delegate.
await metaplex.nfts().delegate({
    nftOrSft,
    authority: updateAuthority,
    delegate: {
        type: 'CollectionV1',
        delegate: collectionDelegate.publicKey,
        updateAuthority: updateAuthority.publicKey,
    },
});

// Token delegate (for programmable NFTs only).
await metaplex.nfts().delegate({
    nftOrSft,
    authority: nftOwner,
    delegate: {
        type: 'TransferV1',
        delegate: transferDelegate.publicKey,
        owner: nftOwner.publicKey,
        data: { amount: 1 },
    },
});

Revoke

Revoke a delegated authority. Note that only metadata delegates can be self-revoked.

// Metadata delegate.
await metaplex.nfts().revoke({
    nftOrSft,
    authority: updateAuthority,
    delegate: {
        type: 'CollectionV1',
        delegate: collectionDelegate.publicKey,
        updateAuthority: updateAuthority.publicKey,
    },
});

// Token delegate (for programmable NFTs only).
await metaplex.nfts().revoke({
    nftOrSft,
    authority: nftOwner,
    delegate: {
        type: 'TransferV1',
        delegate: transferDelegate.publicKey,
        owner: nftOwner.publicKey,
    },
});

// Metadata delegate self-revoke.
await metaplex.nfts().revoke({
    nftOrSft,
    authority: { __kind: 'self', delegate: collectionDelegate },
    delegate: {
        type: 'CollectionV1',
        delegate: collectionDelegate.publicKey,
        updateAuthority: nft.updateAuthorityAddress,
    },
});

Lock/Unlock

Allow specific delegates to lock and unlock programmable NFTs. This is for programmable NFTs only.

// Lock an NFT using a utility delegate.
await metaplex.nfts().lock({
    nftOrSft: nft,
    authority: {
        __kind: 'tokenDelegate',
        type: 'UtilityV1',
        delegate: utilityDelegate,
        owner: nftOwner.publicKey,
    },
});

// Unlock an NFT using a utility delegate.
await metaplex.nfts().unlock({
    nftOrSft: nft,
    authority: {
        __kind: 'tokenDelegate',
        type: 'UtilityV1',
        delegate: utilityDelegate,
        owner: nftOwner.publicKey,
    },
});

js's People

Contributors

acheroncrypto avatar altar12 avatar anoushk1234 avatar antey13 avatar blockiosaurus avatar coachchuckff avatar cosimo-rip avatar dataknox avatar febo avatar github-actions[bot] avatar jessetherobot avatar jnwng avatar kartiksoneji avatar kquirapas avatar lorisleiva avatar mjzuppe avatar nhanphan avatar nicholas-ewasiuk avatar nickfrosty avatar ovasylenko avatar samuelvanderwaal avatar sdlaver avatar stegabob avatar steveluscher avatar stranzhay avatar the-mercenaries-ltd avatar thlorenz avatar tjkyner avatar yi-fan-song avatar zaxozhu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

js's Issues

Can't import @metaplex-foundation/js-next: Error: Directory import

Error: Directory import '~/flex/node_modules/@metaplex-foundation/js-next/dist/esm/drivers' is not supported resolving ES modules imported from ~/flex/node_modules/@metaplex-foundation/js-next/dist/esm/index.js

Trying to import the NPM library into my TypeScript project: import { Metaplex, keypairIdentity } from "@metaplex-foundation/js-next";

FindNftByMint fails if the URI is invalid

That's because findNftByMint will automatically run the metadataTask which loads the JSON metadata in the NFT object. When the URI is invalid, that task rightfully fails by throwing an error (ensuring the right status on the task).

However, users should still be able to access NFTs with invalid URIs. For instance, they might access an NFT they are planning on burning (See #72 (comment)).

The solution here is to wrap the metadataTask.run() call inside a try/catch in the findNftByMint method / operation handler.

Autogenerate TransactionBuilders

Could be:

  • Inside this repo
  • As a separate set of packages with swaggy name. e.g. intent, intent-mpl, intent-spl, etc.
  • As part of solita

To explore

Small candy machine changes

As discussed with @thlorenz on a call:

  • Remove try prefixes in methods. Not comparable to Rust.
  • Refactor tryConvertToPublickKey to something more generic that accepts more value like toPublicKey.
  • Provide a DateTime input type that includes multiple formats like Date | number | string.
  • Refactor UnreachableCaseError to a SdkError which extends MetaplexError. Give the unreachable case as parameter.
  • Everything must go through the rpc() driver. Using connection directly is a smell.
  • Spread the data when creating a candy machine and make as many things as possible optional whilst documenting the defaults value.

Registry failed to return tag "buffer" with Yarn 2 and 3

A nested dependency uses the npm:buffer alias as a dependency without providing a version range which causes the SDK installation to fail on Yarn version 2 and 3 (See yarnpkg/berry#1816).

buffer@npm:buffer: Registry failed to return tag "buffer"

The dependency that uses that tag is https://github.com/Bundlr-Network/arbundles which is a dependency of https://github.com/Bundlr-Network/js-client

Dependency tree

Steps to reproduce

For Yarn 3

mkdir js-next-yarn-3
cd js-next-yarn-3
yarn set version stable
yarn -v # <- 3.2.0
yarn add @metaplex-foundation/js-next # <- Error

For Yarn 2

mkdir js-next-yarn-2
cd js-next-yarn-2
yarn set version 2.x
yarn -v # <- 2.4.3
yarn add @metaplex-foundation/js-next # <- Error

Solution

We need to upgrade our @bundlr-network/client dependency to 0.8.0-rc1. (Checking with Josh first as it's not a stable version yet). 0.7.3.

Version 0.7.2 of @bundlr-network/client uses version 0.6.17-rc1 of arbundles which is requires the npm:buffer dependency.

Version 0.8.0-rc1 of @bundlr-network/client uses version 0.7.0-rc2 of arbundles which only requires the npm:buffer as a dev dependency and therefore solves this issue.

EDIT: Version 0.7.3 now also fixes this issue after talking to Josh.

Fetch NFTs in bulk

  • Fetch multiple NFTs from an array of mint public keys.
  • Fetch all NFTs by owner.
  • Refactor NFT to include lazy loaders so that we don't have to download the JSON metadata for all NFTs before returning (bad UX).
  • Test
  • Document: Loaders and MetaplexFile

Find a way to make the FilesystemDriver work on browser

If the NodeFilesystemDriver gets imported into a web app, compiling will fail because fs does not exist.

We need a way to always exclude this in the browser and ideally other implementations of the Filesystem that can be used as a replacement in the browser.

Alternatively, abstracting the filesystem might not bring much value for the SDK for now so we could just remove this driver altogether (but keep the MetaplexFile which is used in StorageDrivers).

Parse transaction errors using Cusper when applicable

This requires some program knowledge and a mapping between instruction number and program. Maybe introducing a collection of ProgramManifest stored on the Metaplex instance could be an elegant way to solve this.

uploadMetadata(...) fails: MetaplexError: Bundlr > Failed to Initialize Bundlr

See code below; fails at uploadMetadata(...)

const {
  Metaplex,
  keypairIdentity,
  bundlrStorage,
} = require("@metaplex-foundation/js-next");

async function mint(keypair: Keypair) {
  if (!keypair) {
    return;
  }
  const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(keypair))
    .use(bundlrStorage());

  const { uri } = await metaplex.nfts().uploadMetadata({
    name: "My NFT",
    description: "My description",
    image:
      "https://www.arweave.net/VsKHsd9aJa2IZ82aYpcBuoT2PmXLifuM4HeeSYtRJj8?ext=jpeg",
  });

  console.log(uri);

Crash logs:

Unhandled Runtime Error
MetaplexError: Bundlr > Failed to Initialize Bundlr
>> Source: Plugin > Bundlr
>> Problem: Bundlr could not be initialized.
>> Solution: This could happen for a variety of reasons. Check the underlying error below for more information.

Caused By: TypeError: Cannot read properties of undefined (reading 'toBuffer')

Screen Shot 2022-05-01 at 3 32 29 PM

Autogenerate GpaBuilders

Similarly to Transaction builders, this could stay in this repo, be extracted into a series of repos or be part of solita. to explore.

[chore]: complete a test to verify that data is updated on chain

Currently tests only check for return value of create|update Nft calls which means that they would not break even if those functions wouldn't communicate at all to the programs.

We need to have all tests check for on-chain data. I will add this check to one of the existing tests . This can serve as an example for anyone wanting to do this for the remaining tests as well.

Handle batches of wallet approval

Find a way to improve both the UX and the DevX when signing multiple transactions and messages that simply can't be bulk signed using signAllTransactions.

For example, when minting an NFT using Bundlr, it will require 5 approvals:

  • One for signing the fund transaction of the first upload (the image)
  • One for signing a message allowing Bundlr to upload it.
  • One for signing the fund transaction of the second upload (the metadata referencing the first upload).
  • One for signing a message allowing Bundlr to upload it.
  • One for signing the transaction that will create the NFT.

(Note that the two funding transactions could be merged into one)

In these situations, the user will inevitably end up having to click "approve" multiple times. A better UX would be to provide them with a modal like the one below where they can see what they are going to approve and why.

image

The SDK should probably facilitate that for the developers by providing an abstraction layer on top of "batches of approval" Something that would accept an Array<() => Promise<T>> and keep track of which ones were triggered.

Nothing set in stone, we'd need to discuss in more detail how this would look like concretely by gathering more use cases.

Upload multiple files in one go

Add support for uploading more than one file in the StorageDriver abstract class.

Could be with another method or by allowing more than one file to be passed into upload using a variadic parameter.

Similarly, we'd need to update the getPrice method to accept multiple files or make it more generic by accepting a number of bytes instead.

Error [ERR_MODULE_NOT_FOUND] importing { Metaplex } from "@metaplex-foundation/js-next"

Discussed in #63

Originally posted by katewayo April 20, 2022
Hi! Hope all is well. I'm building a node + express server application that's trying to call the new API. I don't have any issues when importing things like @solana/web3.js and bip39, but unfortunately I'm getting an error at node initialization when importing Metaplex.

Here's the error:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/kirtd/thesyncfloor/apps/hermes/node_modules/@metaplex-foundation/js-next/dist/esm/index.js' imported from /Users/kirtd/thesyncfloor/apps/hermes/solana.js**

And here's the import line:

import { Metaplex } from "@metaplex-foundation/js-next";

Any ideas?

Thanks in advance,

Kirt.
enderzshadow.sol
@katewayo on Twitter

Refactor signers of transaction builders

Make it simpler.

For instance Web3Signer | PublicKey and PublicKey gets ignored when trying to add as signer.

Or maybe, Web3Signer | { publicKey: PublicKey, sign(tx: Transaction): void } so we can make it compatible with IdentityDrivers without making them coupled.

Note that transaction builders could end up being auto-generated and extracted from this repo.

Consolidate Task and Plan APIs

Between Plans, Steps, Loaders, Operation, OperationHandlers, etc. a few components are trying to tackle asynchronous tasks and they might benefit from being consolidated into one reusable low-level API.

Additionally, this would make it easier to handle AbortSignals since they will all share the same API.

[Idea] Create Bundler-specific plugins for web apps

Bundling our own library for browser applications is usually a bad idea because it outputs one large file that cannot be tree-shaken β€” That's what web3.js does and lots of devs are not happy about it.

However, if we don't, we push the responsibility of polyfilling missing node dependencies to our users which hurts the DX.

This is because most modern web bundlers (such as Webpack 5, Rollup, Vite) no longer polyfills node dependencies out of the box for us.

One way around this is to document how to use our SDK with all modern web bundlers.

A better way around this would be to provide plugins for each bundler that does all of that for you.

Definitely a Nice-to-have but might be worse considering.

Rename API methods in the Candy Machine module

As per PR #99, this is a reminder that the same needs to happen for the Candy Machine module.

// Current API.
metaplex.candyMachines().findCandyMachineByAddress()
metaplex.candyMachines().findCandyMachinesByWallet()
metaplex.candyMachines().findCandyMachinesByAuthority()
metaplex.candyMachines().createCandyMachine()
metaplex.candyMachines().createCandyMachineFromConfig()

// Proposed API.
metaplex.candyMachines().findByAddress()
metaplex.candyMachines().findAllByWallet()
metaplex.candyMachines().findAllByAuthority()
metaplex.candyMachines().create()
metaplex.candyMachines().createFromConfig()

This only affects the methods of the Candy Machine clients, the name of the operations themselves should stay the same since they are exported without additional context.

Refactor API to use Operations, Plans and Plugins

This PR aims to add a bit of CQRS to the SDK to improve its extendability via plugins.

So far the only things that can be extended are "Drivers" (low-level strategy pattern, so far only abstracting wallet identities and storage providers).

Since operations on Solana are clearly either read or write, using Command and Query handlers fits quite well within the domain of this SDK. It will allow plugins to implement their own handlers and swap the default ones.

It's important to notice that the high-level API used by 90% of developers will stay as user-friendly as possible but this would make the underlying logic more flexible. Here's a quick diagram illustrating this.

JSSDK CQRS + Plan Proposal

Implement the updateNft action

Given an Nft object, allows the user to update many aspects of this NFT by providing a partial payload of what they want to update. All attributes of that payload should be flattened (i.e. they can pass "name" instead of "data.name").

Brandon is already working on a PR for this. Since he's very busy with Oddkey at the moment we can merge what he's got and finish the rest. πŸ™‚

error when calling findNftsByCreator

Hi

I get the following error when I call this two lines:
const creator = new PublicKey("D9vTmH4hDS66L54dHit7keVWwtEJuyfFG3Ey5hHJRXHH")
const nfts = await metaplex.nfts().findNftsByCreator(creator);

Error: 410 Gone: {"jsonrpc":"2.0","error":{"code": 410, "message":"The RPC call or parameters have been disabled."}, "id": "fda19491-5acf-4adf-8dce-899014858bab" }

Same for findNftsByCandyMachine().

findNftByMint() on the other hand works fine.

Am I doing it wrong?

Add support for "using NFTs"

The Token Metadata program allows for NFTs to be "usable". Meaning, one can set a number of "uses" an NFT has and decrease it on-chain via the Utilize instruction.

The aim of this issue is to support "utilizing" NFTs in the SDK within the NFT module.

The API might look something like this:

// Adds one usage to the NFT.
metaplex.nfts().use(nft);

// Adds three usages to the NFT.
metaplex.nfts().use(nft, 3);

Recognise MetaplexFile instances in provided JSON and automatically upload them

Basically simplifying the usage from this:

const url = await metaplex.value.storage().upload(new MetaplexFile(someFileBuffer, someFileName));
await metaplex.value.nfts().createNft({
    metadata: {
        name: 'My NFT',
        description: 'My NFT description',
        image: url,
        // ...
    },
});

To this:

await metaplex.value.nfts().createNft({
    metadata: {
        name: 'My NFT',
        description: 'My NFT description',
        image: new MetaplexFile(someFileBuffer, someFileName),
        // ...
    },
});

Withdraw money from Bundlr's wallet

After uploading assets, withdraw the excess money funded using: withdraw(getBalance() - 5000) or better yet withdrawAll when it's available.

We need to be mindful that if we withdraw after each upload, the uploadMetadata operation would need to fund twice, therefore, rendering its current optimisation useless.

Fix issue with uploadMetadata and bundlrStorage driver

Environment:
Node v17.2
Ubuntu 20.04 LTS

Issue:
When performing the uploadMetadata operation, the Buffer appears to be passed the KeypairIdentityDriver at some point.

Sample code:
`

const { Metaplex, MetaplexFile, keypairIdentity, bundlrStorage, mockStorage } = require("@metaplex-foundation/js-next");
const { Connection, clusterApiUrl, Keypair } = require("@solana/web3.js");
const fs = require('fs')
const path = require('path')
const connection = new Connection(clusterApiUrl("devnet"));

let keypairFile = fs.readFileSync('/home/knox/jsnext/newkey.json')
const keypair = Keypair.fromSecretKey(Buffer.from(JSON.parse(keypairFile.toString())));

const metaplex = Metaplex.make(connection)
    .use(keypairIdentity(keypair))
    .use(bundlrStorage({
        address: 'https://devnet.bundlr.network',
        providerUrl: 'https://api.devnet.solana.com',
        timeout: 60000,
    }))


const artPath = '/home/knox/jsnext/art/'
async function batchCreator() {
    //const mx = await metaplex();
    const { uri, metadata } = await metaplex.nfts().uploadMetadata({
        name: "My NFT #",
        image: new MetaplexFile(Buffer.from('/home/knox/jsnext/art/1.png'), '1.png'),
        properties: {
            files: [
                {
                    type: "image/png",
                    uri: new MetaplexFile(Buffer.from('/home/knox/jsnext/art/1.png'), '1.png'),
                },
            ]
        }
    });

    console.log(metadata.image) // https://arweave.net/123
    console.log(metadata.properties.files[0].uri) // https://arweave.net/456
    console.log(uri) // https://arweave.net/789
}



batchCreator();

`

Result:
bigint: Failed to load bindings, pure JS will be used (try npm run rebuild?)
(node:1327) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use node --trace-warnings ... to show where the warning was created)
node:internal/errors:464
ErrorCaptureStackTrace(err);
^

TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received an instance of KeypairIdentityDriver
at new NodeError (node:internal/errors:371:5)
at Function.from (node:buffer:322:9)
at SolanaConfig.getKeyPair (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/currencies/solana.js:54:48)
at SolanaConfig.getPublicKey (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/currencies/solana.js:159:26)
at new BaseNodeCurrency (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/currency.js:16:64)
at new SolanaConfig (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/currencies/solana.js:39:9)
at getCurrency (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/currencies/index.js:28:20)
at new NodeBundlr (/home/knox/jsnext/js-next-examples/node_modules/@bundlr-network/client/build/node/bundlr.js:24:56)
at BundlrStorageDriver. (/home/knox/jsnext/js-next-examples/node_modules/@metaplex-foundation/js-next/dist/cjs/drivers/storage/BundlrStorageDriver.js:143:19)
at Generator.next () {
code: 'ERR_INVALID_ARG_TYPE'
}

NOTES
The operation works fine with mockStorage(). I will test with aws and see if this has any impact

UpdateNft sets isMutable to true by default

When using the updateNft function youll get an error if you dont specify isMutable:false in the UpdateNftOperation params. You can't set isMutable to true once the NFT is created whether you set isMutable to true or false while creating it

Solution would be to set the value to false by default in update params

Add Logging drivers

Add an abstract and concrete implementation of drivers that can log things that happen within the SDK and output useful metrics from them.

Some brainstorming needs to happen here to clarify what this would look like.

Node 18.1 Named export 'EventEmitter' not found

Hi @lorisleiva, was going in to test this with a vanilla node project (v18.1) (working example to reproduce: https://github.com/mjzuppe/js-next-er) and have two issues:

1.) I get ES modules errors, but I can work around by running node --experimental-specifier-resolution=node
2.) I'm blocked on this error:

import { EventEmitter } from 'eventemitter3';
         ^^^^^^^^^^^^
SyntaxError: Named export 'EventEmitter' not found. The requested module 'eventemitter3' is a CommonJS module, which may not support all module.exports as named exports.

seems to be originating from/js-next/node_modules/@metaplex-foundation/js-next/dist/esm/shared/useDisposable.js:1

Add Error Handling drivers

Create an abstract and concrete implementation of a driver that can catch and handle errors.

The idea is someone could push their own error handling driver so that when an error occurs it gets sent to Sentry and displayed as a toast to the user. A default dummy driver could simply console log.

NPM vs GitHub Package Naming Discrepancy Resulting in Import Error?

The readme has the user install npm install @metaplex-foundation/js-next but then import statement within the JS file is instructed to be import { Metaplex } from "@metaplex/js-next";!

The import statement fails for me when I import as stated, but even if I change it to import { Metaplex } from "@metaplex-foundation/js-next";, it still is erroring for me. I am running Node V16.

import { Metaplex } from "@metaplex-foundation/js-next";
SyntaxError: Named export 'Metaplex' not found. The requested module '@metaplex-foundation/js-next' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from @metaplex-foundation/js-next;
const { Metaplex } = pkg;

Detect protocol when downloading assets

Not super urgent but we'll eventually need an extendable way to download assets that are not simply using the http(s) protocols. For instance, someone could pass in an ipfs:// URI and the SDK would need to know how to resolve it.

Having a method such as addProtocolResolver(protocol, callback) could be part of the solution.

Currently, the abstract StorageDriver deals with downloading since it enforces an upload method and it makes sense to have them together semantically.

One solution could be to add this logic to the abstract StorageDriver so all drivers inherit from this logic. However, it might be cleaner to separate the upload and download logic since the latter is independent of the storage provider. The StorageDriver could become the UploadDriver and we'd have another driver type called DownloadDriver. These are just raw ideas I have in mind, nothing set in stone.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.