├── .ammanrc.js ├── .changeset ├── README.md └── config.json ├── .eslintrc ├── .github ├── actions │ └── install-solana │ │ └── action.yml ├── configs │ └── issue-label-automations.yml └── workflows │ ├── label-automations.yml │ ├── lint.yml │ ├── release.yaml │ └── tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── babel.config.json ├── infra ├── generate-new-package.mjs ├── new-package-template │ ├── README.md │ ├── babel.config.json │ ├── package.json.stub │ ├── rollup.config.js │ ├── src │ │ └── index.ts │ ├── test │ │ ├── someFeature.test.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── run-filtered-tests.sh └── typedoc-plugin.js ├── package.json ├── packages ├── js-plugin-aws │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── AwsStorageDriver.ts │ │ ├── index.ts │ │ └── plugin.ts │ ├── test │ │ ├── AwsStorageDriver.test.ts │ │ ├── helpers.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── js-plugin-nft-storage │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── BlockstoreCarReader.ts │ │ ├── NftStorageDriver.ts │ │ ├── index.ts │ │ ├── plugin.ts │ │ └── utils.ts │ ├── test │ │ ├── NftStorageDriver.test.ts │ │ ├── helpers.ts │ │ └── tsconfig.json │ └── tsconfig.json └── js │ ├── .yarnrc │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── Metaplex.ts │ ├── errors │ │ ├── IrysError.ts │ │ ├── MetaplexError.ts │ │ ├── ProgramError.ts │ │ ├── ReadApiError.ts │ │ ├── RpcError.ts │ │ ├── SdkError.ts │ │ └── index.ts │ ├── index.ts │ ├── plugins │ │ ├── auctionHouseModule │ │ │ ├── AuctionHouseBuildersClient.ts │ │ │ ├── AuctionHouseClient.ts │ │ │ ├── AuctionHousePdasClient.ts │ │ │ ├── accounts.ts │ │ │ ├── constants.ts │ │ │ ├── errors.ts │ │ │ ├── gpaBuilders │ │ │ │ ├── BidReceiptGpaBuilder.ts │ │ │ │ ├── ListingReceiptGpaBuilder.ts │ │ │ │ ├── PurchaseReceiptGpaBuilder.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── AuctionHouse.ts │ │ │ │ ├── Bid.ts │ │ │ │ ├── Listing.ts │ │ │ │ ├── Purchase.ts │ │ │ │ └── index.ts │ │ │ ├── operations │ │ │ │ ├── cancelBid.ts │ │ │ │ ├── cancelListing.ts │ │ │ │ ├── createAuctionHouse.ts │ │ │ │ ├── createBid.ts │ │ │ │ ├── createListing.ts │ │ │ │ ├── depositToBuyerAccount.ts │ │ │ │ ├── directBuy.ts │ │ │ │ ├── directSell.ts │ │ │ │ ├── executeSale.ts │ │ │ │ ├── findAuctionHouseByAddress.ts │ │ │ │ ├── findAuctionHouseByCreatorAndMint.ts │ │ │ │ ├── findBidByReceipt.ts │ │ │ │ ├── findBidByTradeState.ts │ │ │ │ ├── findBids.ts │ │ │ │ ├── findListingByReceipt.ts │ │ │ │ ├── findListingByTradeState.ts │ │ │ │ ├── findListings.ts │ │ │ │ ├── findPurchaseByReceipt.ts │ │ │ │ ├── findPurchaseByTradeState.ts │ │ │ │ ├── findPurchases.ts │ │ │ │ ├── getBuyerBalance.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loadBid.ts │ │ │ │ ├── loadListing.ts │ │ │ │ ├── loadPurchase.ts │ │ │ │ ├── updateAuctionHouse.ts │ │ │ │ ├── withdrawFromBuyerAccount.ts │ │ │ │ ├── withdrawFromFeeAccount.ts │ │ │ │ └── withdrawFromTreasuryAccount.ts │ │ │ └── plugin.ts │ │ ├── candyMachineModule │ │ │ ├── CandyMachineBuildersClient.ts │ │ │ ├── CandyMachineClient.ts │ │ │ ├── CandyMachineGuardsClient.ts │ │ │ ├── CandyMachinePdasClient.ts │ │ │ ├── asserts.ts │ │ │ ├── constants.ts │ │ │ ├── errors.ts │ │ │ ├── guards │ │ │ │ ├── addressGate.ts │ │ │ │ ├── allowList.ts │ │ │ │ ├── botTax.ts │ │ │ │ ├── core.ts │ │ │ │ ├── default.ts │ │ │ │ ├── endDate.ts │ │ │ │ ├── freezeSolPayment.ts │ │ │ │ ├── freezeTokenPayment.ts │ │ │ │ ├── gatekeeper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mintLimit.ts │ │ │ │ ├── nftBurn.ts │ │ │ │ ├── nftGate.ts │ │ │ │ ├── nftPayment.ts │ │ │ │ ├── programGate.ts │ │ │ │ ├── redeemedAmount.ts │ │ │ │ ├── solPayment.ts │ │ │ │ ├── startDate.ts │ │ │ │ ├── thirdPartySigner.ts │ │ │ │ ├── tokenBurn.ts │ │ │ │ ├── tokenGate.ts │ │ │ │ └── tokenPayment.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── CandyGuard.ts │ │ │ │ ├── CandyMachine.ts │ │ │ │ ├── CandyMachineHiddenSection.ts │ │ │ │ ├── CandyMachineItem.ts │ │ │ │ └── index.ts │ │ │ ├── operations │ │ │ │ ├── callCandyGuardRoute.ts │ │ │ │ ├── createCandyGuard.ts │ │ │ │ ├── createCandyMachine.ts │ │ │ │ ├── deleteCandyGuard.ts │ │ │ │ ├── deleteCandyMachine.ts │ │ │ │ ├── findCandyGuardByAddress.ts │ │ │ │ ├── findCandyGuardsByAuthority.ts │ │ │ │ ├── findCandyMachineByAddress.ts │ │ │ │ ├── index.ts │ │ │ │ ├── insertCandyMachineItems.ts │ │ │ │ ├── mintFromCandyMachine.ts │ │ │ │ ├── unwrapCandyGuard.ts │ │ │ │ ├── updateCandyGuard.ts │ │ │ │ ├── updateCandyGuardAuthority.ts │ │ │ │ ├── updateCandyMachine.ts │ │ │ │ └── wrapCandyGuard.ts │ │ │ ├── plugin.ts │ │ │ └── programs.ts │ │ ├── candyMachineV2Module │ │ │ ├── CandyMachinesV2BuildersClient.ts │ │ │ ├── CandyMachinesV2Client.ts │ │ │ ├── accounts.ts │ │ │ ├── asserts.ts │ │ │ ├── constants.ts │ │ │ ├── errors.ts │ │ │ ├── gpaBuilders.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── CandyMachineV2.ts │ │ │ │ └── index.ts │ │ │ ├── operations │ │ │ │ ├── createCandyMachineV2.ts │ │ │ │ ├── deleteCandyMachineV2.ts │ │ │ │ ├── findCandyMachineV2ByAddress.ts │ │ │ │ ├── findCandyMachinesV2ByPublicKeyField.ts │ │ │ │ ├── findMintedNftsByCandyMachineV2.ts │ │ │ │ ├── index.ts │ │ │ │ ├── insertItemsToCandyMachineV2.ts │ │ │ │ ├── mintCandyMachineV2.ts │ │ │ │ └── updateCandyMachineV2.ts │ │ │ ├── pdas.ts │ │ │ ├── plugin.ts │ │ │ └── program.ts │ │ ├── corePlugins │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── derivedIdentity │ │ │ ├── DerivedIdentityClient.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── guestIdentity │ │ │ ├── GuestIdentityDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── identityModule │ │ │ ├── IdentityClient.ts │ │ │ ├── IdentityDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── index.ts │ │ ├── irysStorage │ │ │ ├── IrysStorageDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── keypairIdentity │ │ │ ├── KeypairIdentityDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── mockStorage │ │ │ ├── MockStorageDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── nftModule │ │ │ ├── Authorization.ts │ │ │ ├── DelegateInput.ts │ │ │ ├── DelegateType.ts │ │ │ ├── NftBuildersClient.ts │ │ │ ├── NftClient.ts │ │ │ ├── NftPdasClient.ts │ │ │ ├── accounts.ts │ │ │ ├── errors.ts │ │ │ ├── gpaBuilders.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── JsonMetadata.ts │ │ │ │ ├── Metadata.ts │ │ │ │ ├── Nft.ts │ │ │ │ ├── NftEdition.ts │ │ │ │ ├── Sft.ts │ │ │ │ └── index.ts │ │ │ ├── operations │ │ │ │ ├── approveNftCollectionAuthority.ts │ │ │ │ ├── approveNftDelegate.ts │ │ │ │ ├── approveNftUseAuthority.ts │ │ │ │ ├── createCompressedNft.ts │ │ │ │ ├── createNft.ts │ │ │ │ ├── createSft.ts │ │ │ │ ├── deleteNft.ts │ │ │ │ ├── findNftByAssetId.ts │ │ │ │ ├── findNftByMetadata.ts │ │ │ │ ├── findNftByMint.ts │ │ │ │ ├── findNftByToken.ts │ │ │ │ ├── findNftsByCreator.ts │ │ │ │ ├── findNftsByMintList.ts │ │ │ │ ├── findNftsByOwner.ts │ │ │ │ ├── findNftsByUpdateAuthority.ts │ │ │ │ ├── freezeDelegatedNft.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loadMetadata.ts │ │ │ │ ├── lockNft.ts │ │ │ │ ├── migrateToSizedCollectionNft.ts │ │ │ │ ├── mintNft.ts │ │ │ │ ├── printNewEdition.ts │ │ │ │ ├── revokeNftCollectionAuthority.ts │ │ │ │ ├── revokeNftDelegate.ts │ │ │ │ ├── revokeNftUseAuthority.ts │ │ │ │ ├── thawDelegatedNft.ts │ │ │ │ ├── transferCompressedNft.ts │ │ │ │ ├── transferNft.ts │ │ │ │ ├── unlockNft.ts │ │ │ │ ├── unverifyNftCollection.ts │ │ │ │ ├── unverifyNftCreator.ts │ │ │ │ ├── updateNft.ts │ │ │ │ ├── uploadMetadata.ts │ │ │ │ ├── useNft.ts │ │ │ │ ├── verifyNftCollection.ts │ │ │ │ └── verifyNftCreator.ts │ │ │ ├── pdas.ts │ │ │ └── plugin.ts │ │ ├── operationModule │ │ │ ├── OperationClient.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── programModule │ │ │ ├── ProgramClient.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── rpcModule │ │ │ ├── RpcClient.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── storageModule │ │ │ ├── MetaplexFile.ts │ │ │ ├── StorageClient.ts │ │ │ ├── StorageDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ ├── systemModule │ │ │ ├── SystemBuildersClient.ts │ │ │ ├── SystemClient.ts │ │ │ ├── index.ts │ │ │ ├── operations │ │ │ │ ├── createAccount.ts │ │ │ │ ├── index.ts │ │ │ │ └── transferSol.ts │ │ │ └── plugin.ts │ │ ├── tokenModule │ │ │ ├── TokenBuildersClient.ts │ │ │ ├── TokenClient.ts │ │ │ ├── TokenPdasClient.ts │ │ │ ├── accounts.ts │ │ │ ├── constants.ts │ │ │ ├── errors.ts │ │ │ ├── gpaBuilders.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ │ ├── Mint.ts │ │ │ │ ├── Token.ts │ │ │ │ └── index.ts │ │ │ ├── operations │ │ │ │ ├── approveTokenDelegateAuthority.ts │ │ │ │ ├── createMint.ts │ │ │ │ ├── createToken.ts │ │ │ │ ├── createTokenWithMint.ts │ │ │ │ ├── findMintByAddress.ts │ │ │ │ ├── findTokenByAddress.ts │ │ │ │ ├── findTokenWithMintByAddress.ts │ │ │ │ ├── findTokenWithMintByMint.ts │ │ │ │ ├── freezeTokens.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mintTokens.ts │ │ │ │ ├── revokeTokenDelegateAuthority.ts │ │ │ │ ├── sendTokens.ts │ │ │ │ └── thawTokens.ts │ │ │ ├── plugin.ts │ │ │ └── program.ts │ │ ├── utilsModule │ │ │ ├── UtilsClient.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ │ └── walletAdapterIdentity │ │ │ ├── WalletAdapterIdentityDriver.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ ├── types │ │ ├── Account.ts │ │ ├── Amount.ts │ │ ├── BigNumber.ts │ │ ├── Cluster.ts │ │ ├── Creator.ts │ │ ├── DateTime.ts │ │ ├── FeatureFlags.ts │ │ ├── HasDriver.ts │ │ ├── HasMetaplex.ts │ │ ├── MetaplexPlugin.ts │ │ ├── Model.ts │ │ ├── Operation.ts │ │ ├── Pda.ts │ │ ├── Program.ts │ │ ├── PublicKey.ts │ │ ├── ReadApi.ts │ │ ├── Serializer.ts │ │ ├── Signer.ts │ │ └── index.ts │ └── utils │ │ ├── Disposable.ts │ │ ├── GmaBuilder.ts │ │ ├── GpaBuilder.ts │ │ ├── Task.ts │ │ ├── TransactionBuilder.ts │ │ ├── assert.ts │ │ ├── common.ts │ │ ├── exports.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── merkle.ts │ │ ├── readApiConnection.ts │ │ └── types.ts │ ├── test │ ├── cjs-export.test.cjs │ ├── esm-export.test.mjs │ ├── fixtures │ │ └── images.ts │ ├── helpers │ │ ├── amman.ts │ │ ├── asserts.ts │ │ ├── assets.ts │ │ ├── consts.ts │ │ ├── index.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── plugins │ │ ├── auctionHouseModule │ │ │ ├── cancelBid.test.ts │ │ │ ├── cancelListing.test.ts │ │ │ ├── createAuctionHouse.test.ts │ │ │ ├── createBid.test.ts │ │ │ ├── createListing.test.ts │ │ │ ├── depositToBuyerAccount.test.ts │ │ │ ├── directBuy.test.ts │ │ │ ├── directSell.test.ts │ │ │ ├── executePartialSale.test.ts │ │ │ ├── executeSale.test.ts │ │ │ ├── findBids.test.ts │ │ │ ├── findListings.test.ts │ │ │ ├── findPurchases.test.ts │ │ │ ├── helpers.ts │ │ │ ├── updateAuctionHouse.test.ts │ │ │ ├── withdrawFromBuyerAccount.test.ts │ │ │ ├── withdrawFromFeeAccount.test.ts │ │ │ └── withdrawFromTreasuryAccount.test.ts │ │ ├── candyMachineModule │ │ │ ├── callCandyGuardRoute.test.ts │ │ │ ├── createCandyGuard.test.ts │ │ │ ├── createCandyMachine.test.ts │ │ │ ├── deleteCandyGuard.test.ts │ │ │ ├── deleteCandyMachine.test.ts │ │ │ ├── findCandyGuardsByAuthority.test.ts │ │ │ ├── guards │ │ │ │ ├── addressGate.test.ts │ │ │ │ ├── allowList.test.ts │ │ │ │ ├── botTax.test.ts │ │ │ │ ├── endDate.test.ts │ │ │ │ ├── freezeSolPayment.test.ts │ │ │ │ ├── freezeTokenPayment.test.ts │ │ │ │ ├── gatekeeper.test.ts │ │ │ │ ├── mintLimit.test.ts │ │ │ │ ├── nftBurn.test.ts │ │ │ │ ├── nftGate.test.ts │ │ │ │ ├── nftPayment.test.ts │ │ │ │ ├── programGate.test.ts │ │ │ │ ├── redeemedAmount.test.ts │ │ │ │ ├── solPayment.test.ts │ │ │ │ ├── startDate.test.ts │ │ │ │ ├── thirdPartySigner.test.ts │ │ │ │ ├── tokenBurn.test.ts │ │ │ │ ├── tokenGate.test.ts │ │ │ │ └── tokenPayment.test.ts │ │ │ ├── helpers.ts │ │ │ ├── insertCandyMachineItems.test.ts │ │ │ ├── mintFromCandyMachine.test.ts │ │ │ ├── unwrapCandyGuard.test.ts │ │ │ ├── updateCandyGuard.test.ts │ │ │ ├── updateCandyGuardAuthority.test.ts │ │ │ ├── updateCandyMachine.test.ts │ │ │ └── wrapCandyGuard.test.ts │ │ ├── candyMachineV2Module │ │ │ ├── createCandyMachineV2.test.ts │ │ │ ├── deleteCandyMachineV2.test.ts │ │ │ ├── findCandyMachinesV2ByPublicKeyField.test.ts │ │ │ ├── helpers.ts │ │ │ ├── insertItemsToCandyMachineV2.test.ts │ │ │ ├── mintCandyMachineV2.test.ts │ │ │ └── updateCandyMachineV2.test.ts │ │ ├── derivedIdentity │ │ │ └── DerivedIdentityClient.test.ts │ │ ├── nftModule │ │ │ ├── approveNftCollectionAuthority.test.ts │ │ │ ├── approveNftDelegate.test.ts │ │ │ ├── approveNftUseAuthority.test.ts │ │ │ ├── createNft.test.ts │ │ │ ├── createSft.test.ts │ │ │ ├── deleteNft.test.ts │ │ │ ├── findNftByMint.test.ts │ │ │ ├── findNftsByCreator.test.ts │ │ │ ├── findNftsByMintList.test.ts │ │ │ ├── findNftsByOwner.test.ts │ │ │ ├── findNftsByUpdateAuthority.test.ts │ │ │ ├── freezeDelegatedNft.test.ts │ │ │ ├── helpers.ts │ │ │ ├── loadMetadata.test.ts │ │ │ ├── lockNft.test.ts │ │ │ ├── migrateToSizedCollectionNft.test.ts │ │ │ ├── mintNft.test.ts │ │ │ ├── printNewEdition.test.ts │ │ │ ├── revokeNftCollectionAuthority.test.ts │ │ │ ├── revokeNftDelegate.test.ts │ │ │ ├── revokeNftUseAuthority.test.ts │ │ │ ├── thawDelegatedNft.test.ts │ │ │ ├── transferNft.test.ts │ │ │ ├── unlockNft.test.ts │ │ │ ├── unverifyNftCollection.test.ts │ │ │ ├── unverifyNftCreator.test.ts │ │ │ ├── updateNft.test.ts │ │ │ ├── useNft.test.ts │ │ │ ├── verifyNftCollection.test.ts │ │ │ └── verifyNftCreator.test.ts │ │ ├── rpcModule │ │ │ └── RpcClient.test.ts │ │ ├── tokenModule │ │ │ ├── approveTokenDelegateAuthority.test.ts │ │ │ ├── freezeTokens.test.ts │ │ │ ├── helpers.ts │ │ │ ├── mintTokens.test.ts │ │ │ ├── revokeTokenDelegateAuthority.test.ts │ │ │ ├── sendTokens.test.ts │ │ │ └── thawTokens.test.ts │ │ └── utilsModule │ │ │ └── UtilsClient.test.ts │ ├── tsconfig.json │ ├── types │ │ ├── Amount.test.ts │ │ ├── Cluster.test.ts │ │ └── FeatureFlags.test.ts │ └── utils │ │ ├── Disposable.test.ts │ │ └── Task.test.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── programs ├── mpl_auction_house.so ├── mpl_candy_guard.so ├── mpl_candy_machine.so ├── mpl_candy_machine_core.so ├── mpl_token_auth_rules.so ├── mpl_token_metadata.so └── solana_gateway_program.so ├── rollup.config.js ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [["**"]], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/actions/install-solana/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Solana 2 | 3 | inputs: 4 | solana_version: 5 | description: Version of Solana to install 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Cache Solana Install 12 | if: ${{ !env.ACT }} 13 | id: cache-solana-install 14 | uses: actions/cache@v2 15 | with: 16 | path: "$HOME/.local/share/solana/install/releases/${{ inputs.solana_version }}" 17 | key: ${{ runner.os }}-Solana-v${{ inputs.solana_version }} 18 | 19 | - name: Install Solana 20 | if: ${{ !env.ACT }} && steps.cache-solana-install.cache-hit != 'true' 21 | run: | 22 | sh -c "$(curl -sSfL https://release.solana.com/v${{ inputs.solana_version }}/install)" 23 | shell: bash 24 | 25 | - name: Set Active Solana Version 26 | run: | 27 | rm -f "$HOME/.local/share/solana/install/active_release" 28 | ln -s "$HOME/.local/share/solana/install/releases/${{ inputs.solana_version }}/solana-release" "$HOME/.local/share/solana/install/active_release" 29 | shell: bash 30 | 31 | - name: Add Solana bin to Path 32 | run: | 33 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 34 | shell: bash 35 | 36 | - name: Verify Solana install 37 | run: | 38 | solana --version 39 | shell: bash 40 | -------------------------------------------------------------------------------- /.github/workflows/label-automations.yml: -------------------------------------------------------------------------------- 1 | name: "Label Automations" 2 | 3 | on: 4 | issues: 5 | types: [labeled, unlabeled] 6 | discussion: 7 | types: [labeled, unlabeled] 8 | # pull_request: 9 | # types: [labeled, unlabeled] 10 | 11 | permissions: 12 | contents: read 13 | issues: write 14 | discussions: write 15 | # pull-requests: write 16 | 17 | jobs: 18 | issue-label-automations: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: dessant/label-actions@v2 22 | with: 23 | config-path: '.github/configs/issue-label-automations.yml' 24 | process-only: 'issues, discussions' 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Git checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup pnpm 18 | uses: pnpm/action-setup@v2 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | cache: 'pnpm' 25 | 26 | - name: Install dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Prettier format 30 | run: pnpm run format 31 | 32 | - name: Lint fix 33 | run: pnpm run lint:fix 34 | 35 | - name: Commit potential changes 36 | uses: stefanzweifel/git-auto-commit-action@v4 37 | with: 38 | commit_message: Fix styling 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | cache: 'pnpm' 26 | 27 | - name: Install Dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Create Release Pull Request or Publish 31 | id: changesets 32 | uses: changesets/action@v1 33 | with: 34 | title: Release Packages 35 | publish: pnpm packages:publish 36 | createGithubReleases: true 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: ["16.x", "18.x"] 15 | solana: ["1.10.34"] 16 | 17 | steps: 18 | - name: Git checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2 25 | 26 | - name: Install Solana 27 | uses: ./.github/actions/install-solana 28 | with: 29 | solana_version: ${{ matrix.solana }} 30 | 31 | - name: Setup Node.js ${{ matrix.node }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node }} 35 | cache: 'pnpm' 36 | 37 | - name: Install dependencies 38 | run: pnpm install --frozen-lockfile 39 | 40 | - name: Start local validator using Amman 41 | run: pnpm amman:start 42 | 43 | - name: Build and Run tests 44 | run: pnpm test:all 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | docs 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # Bundle stats file 28 | stats.html 29 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/.github 3 | **/node_modules 4 | **/dist 5 | **/docs 6 | 7 | .prettierrc 8 | package.json 9 | tsconfig.cjs.json 10 | tsconfig.json 11 | README.md 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "bracketSpacing": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "useTabs": false, 7 | "tabWidth": 2, 8 | "arrowParens": "always", 9 | "printWidth": 80 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "bugfixes": true 7 | } 8 | ], 9 | ["@babel/preset-typescript"] 10 | ], 11 | "plugins": [ 12 | [ 13 | "module-resolver", 14 | { 15 | "root": ["./src"], 16 | "alias": { 17 | "@": "./src" 18 | } 19 | } 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /infra/generate-new-package.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | /* eslint-disable no-console */ 3 | import fs from 'fs'; 4 | import { dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import chalk from 'chalk'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | const templateFolder = __dirname + '/new-package-template'; 10 | const packageName = process.argv[2]; 11 | 12 | if (!packageName) { 13 | error('Please provide a package name as an argument.'); 14 | } 15 | 16 | const packageFolder = __dirname + '/../packages/' + packageName; 17 | 18 | fs.mkdirSync(packageFolder); 19 | generateFilesRecursively(templateFolder, packageFolder); 20 | success(`Package "${packageName}" generated successfully.`); 21 | 22 | function generateFilesRecursively( 23 | templateFolder, 24 | packageFolder, 25 | currentPath = '' 26 | ) { 27 | const dirents = fs.readdirSync(templateFolder + currentPath, { 28 | withFileTypes: true, 29 | }); 30 | 31 | dirents.forEach((dirent) => { 32 | const newPath = currentPath + '/' + dirent.name; 33 | const newPathWithoutStubExtension = newPath.replace(/\.stub$/, ''); 34 | 35 | if (dirent.isDirectory()) { 36 | fs.mkdirSync(packageFolder + newPath); 37 | generateFilesRecursively(templateFolder, packageFolder, newPath); 38 | } else { 39 | let stub = fs.readFileSync(templateFolder + newPath, 'utf8'); 40 | stub = stub.replace(/{{package-name}}/g, packageName); 41 | fs.writeFileSync( 42 | packageFolder + newPathWithoutStubExtension, 43 | stub, 44 | 'utf8' 45 | ); 46 | } 47 | }); 48 | } 49 | 50 | function error(message) { 51 | console.error(chalk.bold.red(message)); 52 | process.exit(1); 53 | } 54 | 55 | function success(message) { 56 | console.log(chalk.bold.green(message)); 57 | process.exit(0); 58 | } 59 | -------------------------------------------------------------------------------- /infra/new-package-template/README.md: -------------------------------------------------------------------------------- 1 | # {{package-name}} 2 | 3 | TODO 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install @metaplex-foundation/{{package-name}} 9 | ``` 10 | -------------------------------------------------------------------------------- /infra/new-package-template/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.json" 3 | } 4 | -------------------------------------------------------------------------------- /infra/new-package-template/package.json.stub: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metaplex-foundation/{{package-name}}", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "sideEffects": false, 6 | "module": "dist/esm/index.mjs", 7 | "main": "dist/cjs/index.cjs", 8 | "types": "dist/types/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/esm/index.mjs", 12 | "require": "./dist/cjs/index.cjs" 13 | } 14 | }, 15 | "files": [ 16 | "/dist/cjs", 17 | "/dist/esm", 18 | "/dist/types", 19 | "/src" 20 | ], 21 | "scripts": { 22 | "lint": "eslint --ext js,ts,tsx src", 23 | "lint:fix": "eslint --fix --ext js,ts,tsx src", 24 | "clean": "rimraf dist", 25 | "build": "pnpm clean && tsc && tsc-alias && tsc -p test/tsconfig.json && tsc-alias -p test/tsconfig.json && rollup -c", 26 | "test": "CI=1 tape dist/test/**/*.test.js" 27 | }, 28 | "dependencies": {}, 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "author": "Metaplex Maintainers ", 33 | "homepage": "https://metaplex.com", 34 | "repository": { 35 | "url": "https://github.com/metaplex-foundation/js.git" 36 | }, 37 | "typedoc": { 38 | "entryPoint": "./src/index.ts", 39 | "readmeFile": "./README.md", 40 | "displayName": "{{package-name}}" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /infra/new-package-template/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigs } from '../../rollup.config'; 2 | import pkg from './package.json'; 3 | 4 | export default createConfigs({ 5 | pkg, 6 | builds: [ 7 | { 8 | dir: 'dist/esm', 9 | format: 'es', 10 | }, 11 | { 12 | dir: 'dist/cjs', 13 | format: 'cjs', 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /infra/new-package-template/src/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /infra/new-package-template/test/someFeature.test.ts: -------------------------------------------------------------------------------- 1 | import test, { Test } from 'tape'; 2 | 3 | test('[packageName] it tests some dummy feature', async (t: Test) => { 4 | t.equal(1, 1); 5 | }); 6 | -------------------------------------------------------------------------------- /infra/new-package-template/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "outDir": "../dist/test", 8 | "declarationDir": null, 9 | "declaration": false, 10 | "emitDeclarationOnly": false, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /infra/new-package-template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "dist/esm", 6 | "declarationDir": "dist/types" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /infra/run-filtered-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # error output colour 4 | RED() { echo $'\e[1;31m'$1$'\e[0m'; } 5 | 6 | if [ ! -z "$1" ]; then 7 | for file in $(find . -path './packages/*/test/*/'$1'.test.ts' -type f); do 8 | esr $file | tap-spec 9 | done 10 | else 11 | echo "$(RED "Error: ")Please specify a test file pattern" 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/.yarnrc: -------------------------------------------------------------------------------- 1 | version-tag-prefix js-plugin-aws/v 2 | version-git-message "tag: js-plugin-aws/v%s" 3 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/README.md: -------------------------------------------------------------------------------- 1 | # AWS Plugin for the Metaplex JavaScript SDK 2 | 3 | This plugin provides a storage driver for the Metaplex JavaScript SDK that uses Amazon Web Services (AWS) to upload assets. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install @metaplex-foundation/js-plugin-aws 9 | ``` 10 | 11 | ### Usage 12 | 13 | The `awsStorage` plugin uploads assets off-chain to an S3 bucket of your choice. 14 | 15 | To set this up, you need to pass in the AWS client as well as the bucket name you wish to use. For instance: 16 | 17 | ```ts 18 | import { awsStorage } from "@metaplex-foundation/js-plugin-aws"; 19 | import { S3Client } from "@aws-sdk/client-s3"; 20 | 21 | const awsClient = new S3Client({ 22 | region: "us-east-1", 23 | credentials: { 24 | accessKeyId: "", 25 | secretAccessKey: "", 26 | }, 27 | }); 28 | 29 | metaplex.use(awsStorage(awsClient, 'my-nft-bucket')); 30 | ``` 31 | 32 | When uploading a `MetaplexFile` using `metaplex.storage().upload(file)`, the unique name of the file will be used as the AWS key. By default, this will be a random string generated by the SDK but you may explicitly provide your own like so. 33 | 34 | ```ts 35 | const file = toMetaplexFile('file-content', 'filename.jpg', { 36 | uniqueName: 'my-unique-aws-key', 37 | }) 38 | 39 | const uri = await metaplex.storage().upload(file); 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metaplex-foundation/js-plugin-aws", 3 | "version": "0.20.0", 4 | "sideEffects": false, 5 | "module": "dist/esm/index.mjs", 6 | "main": "dist/cjs/index.cjs", 7 | "types": "dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/esm/index.mjs", 11 | "require": "./dist/cjs/index.cjs" 12 | } 13 | }, 14 | "files": [ 15 | "/dist/cjs", 16 | "/dist/esm", 17 | "/dist/types", 18 | "/src" 19 | ], 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "license": "MIT", 24 | "description": "Metaplex JavaScript SDK", 25 | "keywords": [ 26 | "nft", 27 | "metaplex", 28 | "solana", 29 | "blockchain", 30 | "plugin", 31 | "aws" 32 | ], 33 | "author": "Metaplex Maintainers ", 34 | "homepage": "https://metaplex.com", 35 | "repository": { 36 | "url": "https://github.com/metaplex-foundation/js.git" 37 | }, 38 | "scripts": { 39 | "lint": "eslint --ext js,ts,tsx src", 40 | "lint:fix": "eslint --fix --ext js,ts,tsx src", 41 | "clean": "rimraf dist", 42 | "build": "pnpm clean && tsc && tsc-alias && tsc -p test/tsconfig.json && tsc-alias -p test/tsconfig.json && rollup -c", 43 | "test": "CI=1 tape dist/test/**/*.test.js" 44 | }, 45 | "dependencies": { 46 | "@aws-sdk/client-s3": "^3.54.1", 47 | "@metaplex-foundation/js": "workspace:*" 48 | }, 49 | "typedoc": { 50 | "entryPoint": "./src/index.ts", 51 | "readmeFile": "./README.md", 52 | "displayName": "js-plugin-aws" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigs } from '../../rollup.config'; 2 | import pkg from './package.json'; 3 | 4 | export default createConfigs({ 5 | pkg, 6 | dependenciesToExcludeInBundle: [ 7 | '@metaplex-foundation/js', 8 | '@aws-sdk/client-s3', 9 | ], 10 | builds: [ 11 | { 12 | dir: 'dist/esm', 13 | format: 'es', 14 | bundle: true, 15 | }, 16 | { 17 | dir: 'dist/cjs', 18 | format: 'cjs', 19 | bundle: true, 20 | }, 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/src/AwsStorageDriver.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 2 | import { 3 | MetaplexFile, 4 | StorageDriver, 5 | lamports, 6 | Amount, 7 | } from '@metaplex-foundation/js'; 8 | 9 | export class AwsStorageDriver implements StorageDriver { 10 | protected client: S3Client; 11 | protected bucketName: string; 12 | 13 | constructor(client: S3Client, bucketName: string) { 14 | this.client = client; 15 | this.bucketName = bucketName; 16 | } 17 | 18 | async getUploadPrice(): Promise { 19 | return lamports(0); 20 | } 21 | 22 | async upload(file: MetaplexFile): Promise { 23 | const command = new PutObjectCommand({ 24 | Bucket: this.bucketName, 25 | Key: file.uniqueName, 26 | Body: file.buffer, 27 | ContentType: file.contentType || undefined, 28 | }); 29 | 30 | try { 31 | await this.client.send(command); 32 | 33 | return await this.getUrl(file.uniqueName); 34 | } catch (err) { 35 | // TODO: Custom errors. 36 | throw err; 37 | } 38 | } 39 | 40 | async getUrl(key: string) { 41 | const region = await this.client.config.region(); 42 | const encodedKey = encodeURIComponent(key); 43 | 44 | return `https://s3.${region}.amazonaws.com/${this.bucketName}/${encodedKey}`; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AwsStorageDriver'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { S3Client } from '@aws-sdk/client-s3'; 2 | import type { Metaplex, MetaplexPlugin } from '@metaplex-foundation/js'; 3 | import { AwsStorageDriver } from './AwsStorageDriver'; 4 | 5 | export const awsStorage = ( 6 | client: S3Client, 7 | bucketName: string 8 | ): MetaplexPlugin => ({ 9 | install(metaplex: Metaplex) { 10 | metaplex.storage().setDriver(new AwsStorageDriver(client, bucketName)); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/test/AwsStorageDriver.test.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 2 | import { toMetaplexFile } from '@metaplex-foundation/js'; 3 | import test, { Test } from 'tape'; 4 | import sinon from 'sinon'; 5 | import { awsStorage } from '../src'; 6 | import { killStuckProcess, metaplex } from './helpers'; 7 | 8 | killStuckProcess(); 9 | 10 | const awsClient = { 11 | async send() { 12 | return {}; 13 | }, 14 | config: { 15 | async region() { 16 | return 'us-east'; 17 | }, 18 | }, 19 | } as unknown as S3Client; 20 | 21 | test('it can upload assets to a S3 bucket', async (t: Test) => { 22 | // Given a mock awsClient. 23 | const stub = sinon.spy(awsClient); 24 | 25 | // Fed to a Metaplex instance. 26 | const mx = await metaplex(); 27 | mx.use(awsStorage(awsClient, 'some-bucket')); 28 | 29 | // When we upload some content to AWS S3. 30 | const file = toMetaplexFile('some-image', 'some-image.jpg', { 31 | uniqueName: 'some-key', 32 | }); 33 | const uri = await mx.storage().upload(file); 34 | 35 | // Then we get the URL of the uploaded asset. 36 | t.equals(uri, 'https://s3.us-east.amazonaws.com/some-bucket/some-key'); 37 | t.assert(stub.send.calledOnce); 38 | const command = stub.send.getCall(0).args[0] as PutObjectCommand; 39 | t.assert(command instanceof PutObjectCommand); 40 | t.equals('some-bucket', command.input.Bucket); 41 | t.equals('some-key', command.input.Key); 42 | t.equals('some-image', command.input.Body?.toString()); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, Keypair } from '@solana/web3.js'; 2 | import { Amman, LOCALHOST } from '@metaplex-foundation/amman-client'; 3 | import test from 'tape'; 4 | import { 5 | Metaplex, 6 | guestIdentity, 7 | keypairIdentity, 8 | mockStorage, 9 | KeypairSigner, 10 | logDebug, 11 | } from '@metaplex-foundation/js'; 12 | 13 | export const amman = Amman.instance({ log: logDebug }); 14 | 15 | export type MetaplexTestOptions = { 16 | rpcEndpoint?: string; 17 | 18 | /** The level of commitment desired when querying the blockchain. */ 19 | commitment?: Commitment; 20 | solsToAirdrop?: number; 21 | }; 22 | 23 | export const metaplexGuest = (options: MetaplexTestOptions = {}) => { 24 | const connection = new Connection(options.rpcEndpoint ?? LOCALHOST, { 25 | commitment: options.commitment ?? 'confirmed', 26 | }); 27 | 28 | return Metaplex.make(connection).use(guestIdentity()).use(mockStorage()); 29 | }; 30 | 31 | export const metaplex = async (options: MetaplexTestOptions = {}) => { 32 | const mx = metaplexGuest(options); 33 | const wallet = await createWallet(mx, options.solsToAirdrop); 34 | 35 | return mx.use(keypairIdentity(wallet as Keypair)); 36 | }; 37 | 38 | export const createWallet = async ( 39 | mx: Metaplex, 40 | solsToAirdrop = 100 41 | ): Promise => { 42 | const wallet = Keypair.generate(); 43 | await amman.airdrop(mx.connection, wallet.publicKey, solsToAirdrop); 44 | 45 | return wallet; 46 | }; 47 | 48 | /** 49 | * This is a workaround the fact that web3.js doesn't close it's socket connection and provides no way to do so. 50 | * Therefore the process hangs for a considerable time after the tests finish, increasing the feedback loop. 51 | * 52 | * This fixes this by exiting the process as soon as all tests are finished. 53 | */ 54 | export function killStuckProcess() { 55 | // Don't do this in CI since we need to ensure we get a non-zero exit code if tests fail 56 | if (process.env.CI == null) { 57 | test.onFinish(() => process.exit(0)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "outDir": "../dist/test", 8 | "declarationDir": null, 9 | "declaration": false, 10 | "emitDeclarationOnly": false, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/js-plugin-aws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "dist/esm", 6 | "declarationDir": "dist/types", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/.yarnrc: -------------------------------------------------------------------------------- 1 | version-tag-prefix js-plugin-nft-storage/v 2 | version-git-message "tag: js-plugin-nft-storage/v%s" 3 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metaplex-foundation/js-plugin-nft-storage", 3 | "version": "0.20.0", 4 | "sideEffects": false, 5 | "module": "dist/esm/index.mjs", 6 | "main": "dist/cjs/index.cjs", 7 | "types": "dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/esm/index.mjs", 11 | "require": "./dist/cjs/index.cjs" 12 | } 13 | }, 14 | "files": [ 15 | "/dist/cjs", 16 | "/dist/esm", 17 | "/dist/types", 18 | "/src" 19 | ], 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "license": "MIT", 24 | "description": "Metaplex JavaScript SDK", 25 | "keywords": [ 26 | "nft", 27 | "metaplex", 28 | "solana", 29 | "blockchain", 30 | "plugin", 31 | "nft.storage" 32 | ], 33 | "author": "Metaplex Maintainers ", 34 | "homepage": "https://metaplex.com", 35 | "repository": { 36 | "url": "https://github.com/metaplex-foundation/js.git" 37 | }, 38 | "scripts": { 39 | "lint": "eslint --ext js,ts,tsx src", 40 | "lint:fix": "eslint --fix --ext js,ts,tsx src", 41 | "clean": "rimraf dist", 42 | "build": "pnpm clean && tsc && tsc-alias && tsc -p test/tsconfig.json && tsc-alias -p test/tsconfig.json && rollup -c", 43 | "test": "CI=1 tape dist/test/**/*.test.js" 44 | }, 45 | "dependencies": { 46 | "@ipld/dag-pb": "^2.1.17", 47 | "@metaplex-foundation/js": "workspace:*", 48 | "@nftstorage/metaplex-auth": "^1.2.0", 49 | "ipfs-car": "^0.7.0", 50 | "ipfs-unixfs": "^6.0.9", 51 | "multiformats": "^9.7.0", 52 | "nft.storage": "^6.4.1", 53 | "node-fetch": "^2.6.7" 54 | }, 55 | "typedoc": { 56 | "entryPoint": "./src/index.ts", 57 | "readmeFile": "./README.md", 58 | "displayName": "js-plugin-nft-storage" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigs } from '../../rollup.config'; 2 | import pkg from './package.json'; 3 | 4 | export default createConfigs({ 5 | pkg, 6 | dependenciesToExcludeInBundle: [ 7 | '@metaplex-foundation/js', 8 | '@ipld/dag-pb', 9 | '@nftstorage/metaplex-auth', 10 | 'ipfs-car', 11 | 'ipfs-unixfs', 12 | 'multiformats', 13 | 'nft.storage', 14 | ], 15 | builds: [ 16 | { 17 | dir: 'dist/esm', 18 | format: 'es', 19 | bundle: true, 20 | }, 21 | { 22 | dir: 'dist/cjs', 23 | format: 'cjs', 24 | bundle: true, 25 | }, 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/src/BlockstoreCarReader.ts: -------------------------------------------------------------------------------- 1 | import type { Blockstore } from 'ipfs-car/blockstore'; 2 | import type { CID } from 'multiformats'; 3 | 4 | /** 5 | * An implementation of the CAR reader interface that is backed by a blockstore. 6 | * @see https://github.com/nftstorage/nft.storage/blob/0fc7e4e73867c437eac54f75f58a808dd4581c47/packages/client/src/bs-car-reader.js 7 | */ 8 | export class BlockstoreCarReader { 9 | _version: number; 10 | _roots: CID[]; 11 | _blockstore: Blockstore; 12 | 13 | constructor(roots: CID[], blockstore: Blockstore, version = 1) { 14 | this._version = version; 15 | this._roots = roots; 16 | this._blockstore = blockstore; 17 | } 18 | 19 | get version() { 20 | return this._version; 21 | } 22 | 23 | get blockstore() { 24 | return this._blockstore; 25 | } 26 | 27 | async getRoots() { 28 | return this._roots; 29 | } 30 | 31 | has(cid: CID) { 32 | return this._blockstore.has(cid); 33 | } 34 | 35 | async get(cid: CID) { 36 | const bytes = await this._blockstore.get(cid); 37 | return { cid, bytes }; 38 | } 39 | 40 | blocks(): ReturnType { 41 | return this._blockstore.blocks(); 42 | } 43 | 44 | async *cids() { 45 | for await (const b of this.blocks()) { 46 | yield b.cid; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NftStorageDriver'; 2 | export * from './plugin'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex, MetaplexPlugin } from '@metaplex-foundation/js'; 2 | import { NftStorageDriver, NftStorageDriverOptions } from './NftStorageDriver'; 3 | 4 | export const nftStorage = ( 5 | options: NftStorageDriverOptions = {} 6 | ): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | metaplex.storage().setDriver(new NftStorageDriver(metaplex, options)); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Blockstore } from 'ipfs-car/blockstore'; 2 | import type { CID } from 'multiformats'; 3 | import { CarReader } from 'nft.storage'; 4 | import * as Block from 'multiformats/block'; 5 | import { sha256 } from 'multiformats/hashes/sha2'; 6 | import * as dagPb from '@ipld/dag-pb'; 7 | import { UnixFS } from 'ipfs-unixfs'; 8 | import { BlockstoreCarReader } from './BlockstoreCarReader'; 9 | 10 | export type EncodedCar = { car: CarReader; cid: CID }; 11 | export type DagPbLink = dagPb.PBLink; 12 | export type DagPbBlock = Block.Block; 13 | 14 | export const DEFAULT_GATEWAY_HOST = 'https://nftstorage.link'; 15 | 16 | export function toGatewayUri( 17 | cid: string, 18 | path = '', 19 | host: string = DEFAULT_GATEWAY_HOST 20 | ): string { 21 | let pathPrefix = `/ipfs/${cid}`; 22 | if (path) { 23 | pathPrefix += '/'; 24 | } 25 | host = host || DEFAULT_GATEWAY_HOST; 26 | const base = new URL(pathPrefix, host); 27 | const u = new URL(path, base); 28 | return u.toString(); 29 | } 30 | 31 | export function toIpfsUri(cid: string, path = ''): string { 32 | const u = new URL(path, `ipfs://${cid}`); 33 | return u.toString(); 34 | } 35 | 36 | export async function toDagPbLink( 37 | node: EncodedCar, 38 | name: string 39 | ): Promise { 40 | const block = await node.car.get(node.cid); 41 | if (!block) { 42 | throw new Error(`invalid CAR: missing block for CID [${node.cid}]`); 43 | } 44 | return dagPb.createLink(name, block.bytes.byteLength, node.cid); 45 | } 46 | 47 | export async function toDirectoryBlock( 48 | links: DagPbLink[] 49 | ): Promise { 50 | const data = new UnixFS({ type: 'directory' }).marshal(); 51 | const value = dagPb.createNode(data, links); 52 | return Block.encode({ value, codec: dagPb, hasher: sha256 }); 53 | } 54 | 55 | export async function toEncodedCar( 56 | block: DagPbBlock, 57 | blockstore: Blockstore 58 | ): Promise { 59 | await blockstore.put(block.cid, block.bytes); 60 | const car = new BlockstoreCarReader([block.cid], blockstore); 61 | const { cid } = block; 62 | 63 | return { car, cid }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, Keypair } from '@solana/web3.js'; 2 | import { Amman, LOCALHOST } from '@metaplex-foundation/amman-client'; 3 | import test from 'tape'; 4 | import { 5 | Metaplex, 6 | guestIdentity, 7 | keypairIdentity, 8 | mockStorage, 9 | KeypairSigner, 10 | logDebug, 11 | } from '@metaplex-foundation/js'; 12 | 13 | export const amman = Amman.instance({ log: logDebug }); 14 | 15 | export type MetaplexTestOptions = { 16 | rpcEndpoint?: string; 17 | 18 | /** The level of commitment desired when querying the blockchain. */ 19 | commitment?: Commitment; 20 | solsToAirdrop?: number; 21 | }; 22 | 23 | export const metaplexGuest = (options: MetaplexTestOptions = {}) => { 24 | const connection = new Connection(options.rpcEndpoint ?? LOCALHOST, { 25 | commitment: options.commitment ?? 'confirmed', 26 | }); 27 | 28 | return Metaplex.make(connection).use(guestIdentity()).use(mockStorage()); 29 | }; 30 | 31 | export const metaplex = async (options: MetaplexTestOptions = {}) => { 32 | const mx = metaplexGuest(options); 33 | const wallet = await createWallet(mx, options.solsToAirdrop); 34 | 35 | return mx.use(keypairIdentity(wallet as Keypair)); 36 | }; 37 | 38 | export const createWallet = async ( 39 | mx: Metaplex, 40 | solsToAirdrop = 100 41 | ): Promise => { 42 | const wallet = Keypair.generate(); 43 | await amman.airdrop(mx.connection, wallet.publicKey, solsToAirdrop); 44 | 45 | return wallet; 46 | }; 47 | 48 | /** 49 | * This is a workaround the fact that web3.js doesn't close it's socket connection and provides no way to do so. 50 | * Therefore the process hangs for a considerable time after the tests finish, increasing the feedback loop. 51 | * 52 | * This fixes this by exiting the process as soon as all tests are finished. 53 | */ 54 | export function killStuckProcess() { 55 | // Don't do this in CI since we need to ensure we get a non-zero exit code if tests fail 56 | if (process.env.CI == null) { 57 | test.onFinish(() => process.exit(0)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "outDir": "../dist/test", 8 | "declarationDir": null, 9 | "declaration": false, 10 | "emitDeclarationOnly": false, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/js-plugin-nft-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "dist/esm", 6 | "declarationDir": "dist/types", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/js/.yarnrc: -------------------------------------------------------------------------------- 1 | version-tag-prefix js/v 2 | version-git-message "tag: js/v%s" 3 | -------------------------------------------------------------------------------- /packages/js/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../babel.config.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/js/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigs } from '../../rollup.config'; 2 | import pkg from './package.json'; 3 | 4 | export default createConfigs({ 5 | pkg, 6 | additionalExternals: ['@noble/hashes/sha3', '@noble/hashes/sha512'], 7 | builds: [ 8 | { 9 | dir: 'dist/esm', 10 | format: 'es', 11 | }, 12 | { 13 | dir: 'dist/cjs', 14 | format: 'cjs', 15 | }, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/js/src/Metaplex.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import { ReadApiConnection } from './utils/readApiConnection'; 3 | import { MetaplexPlugin, Cluster, resolveClusterFromConnection } from '@/types'; 4 | import { corePlugins } from '@/plugins/corePlugins'; 5 | 6 | export type MetaplexOptions = { 7 | cluster?: Cluster; 8 | }; 9 | 10 | export class Metaplex { 11 | /** The connection object from Solana's SDK. */ 12 | public readonly connection: Connection | ReadApiConnection; 13 | 14 | /** The cluster in which the connection endpoint belongs to. */ 15 | public readonly cluster: Cluster; 16 | 17 | constructor(connection: Connection, options: MetaplexOptions = {}) { 18 | this.connection = connection; 19 | this.cluster = options.cluster ?? resolveClusterFromConnection(connection); 20 | this.use(corePlugins()); 21 | } 22 | 23 | static make(connection: Connection, options: MetaplexOptions = {}) { 24 | return new this(connection, options); 25 | } 26 | 27 | use(plugin: MetaplexPlugin) { 28 | plugin.install(this); 29 | 30 | return this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/js/src/errors/IrysError.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexError } from './MetaplexError'; 2 | 3 | /** @group Errors */ 4 | export class IrysError extends MetaplexError { 5 | readonly name: string = 'IrysError'; 6 | constructor(message: string, cause?: Error) { 7 | super(message, 'plugin', 'Irys', cause); 8 | } 9 | } 10 | 11 | /** @group Errors */ 12 | export class FailedToInitializeIrysError extends IrysError { 13 | readonly name: string = 'FailedToInitializeIrysError'; 14 | constructor(cause: Error) { 15 | const message = 16 | 'Irys could not be initialized. ' + 17 | 'Please check the underlying error below for more details.'; 18 | super(message, cause); 19 | } 20 | } 21 | 22 | /** @group Errors */ 23 | export class FailedToConnectToIrysAddressError extends IrysError { 24 | readonly name: string = 'FailedToConnectToIrysAddressError'; 25 | constructor(address: string, cause: Error) { 26 | const message = 27 | `Irys could not connect to the provided address [${address}]. ` + 28 | 'Please ensure the provided address is valid. Some valid addresses include: ' + 29 | '"https://node1.irys.xyz" for mainnet and "https://devnet.irys.xyz" for devnet'; 30 | super(message, cause); 31 | } 32 | } 33 | 34 | /** @group Errors */ 35 | export class AssetUploadFailedError extends IrysError { 36 | readonly name: string = 'AssetUploadFailedError'; 37 | constructor(status: number) { 38 | const message = 39 | `The asset could not be uploaded to the Irys network and ` + 40 | `returned the following status code [${status}].`; 41 | super(message); 42 | } 43 | } 44 | 45 | /** @group Errors */ 46 | export class IrysWithdrawError extends IrysError { 47 | readonly name: string = 'IrysWithdrawError'; 48 | constructor(error: string) { 49 | const message = 50 | `The balance could not be withdrawn from the Irys network and ` + 51 | `returned the following error: ${error}.`; 52 | super(message); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/js/src/errors/MetaplexError.ts: -------------------------------------------------------------------------------- 1 | /** @group Errors */ 2 | export class MetaplexError extends Error { 3 | readonly name: string = 'MetaplexError'; 4 | readonly source: MetaplexErrorSource; 5 | readonly sourceDetails?: string; 6 | readonly cause?: Error; 7 | 8 | constructor( 9 | message: string, 10 | source: MetaplexErrorSource, 11 | sourceDetails?: string, 12 | cause?: Error 13 | ) { 14 | super(message); 15 | this.source = source; 16 | this.sourceDetails = sourceDetails; 17 | this.cause = cause; 18 | this.message = 19 | this.message + 20 | `\n\nSource: ${this.getFullSource()}` + 21 | (this.cause ? `\n\nCaused By: ${this.cause}` : '') + 22 | '\n'; 23 | } 24 | 25 | getCapitalizedSource(): string { 26 | if (this.source === 'sdk' || this.source === 'rpc') { 27 | return this.source.toUpperCase(); 28 | } 29 | 30 | return this.source[0].toUpperCase() + this.source.slice(1); 31 | } 32 | 33 | getFullSource(): string { 34 | const capitalizedSource = this.getCapitalizedSource(); 35 | const sourceDetails = this.sourceDetails ? ` > ${this.sourceDetails}` : ''; 36 | 37 | return capitalizedSource + sourceDetails; 38 | } 39 | 40 | toString() { 41 | return `[${this.name}] ${this.message}`; 42 | } 43 | } 44 | 45 | /** @group Errors */ 46 | export type MetaplexErrorSource = 47 | | 'sdk' 48 | | 'network' 49 | | 'rpc' 50 | | 'plugin' 51 | | 'program'; 52 | -------------------------------------------------------------------------------- /packages/js/src/errors/ProgramError.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexError } from './MetaplexError'; 2 | import { Program } from '@/types'; 3 | 4 | /** @group Errors */ 5 | export class ProgramError extends MetaplexError { 6 | readonly name: string = 'ProgramError'; 7 | readonly program: Program; 8 | readonly logs?: string[]; 9 | 10 | constructor( 11 | message: string, 12 | program: Program, 13 | cause?: Error, 14 | logs?: string[] 15 | ) { 16 | super( 17 | message, 18 | 'program', 19 | `${program.name} [${program.address.toString()}]`, 20 | cause 21 | ); 22 | this.program = program; 23 | this.logs = logs; 24 | if (logs) { 25 | this.message = 26 | this.message + 27 | `\nProgram Logs:\n${logs.map((log) => '| ' + log).join('\n')}\n`; 28 | } 29 | } 30 | } 31 | 32 | type UnderlyingProgramError = Error & { code?: number; logs?: string[] }; 33 | 34 | /** @group Errors */ 35 | export class ParsedProgramError extends ProgramError { 36 | readonly name: string = 'ParsedProgramError'; 37 | constructor(program: Program, cause: UnderlyingProgramError, logs: string[]) { 38 | const ofCode = cause.code ? ` of code [${cause.code}]` : ''; 39 | const message = 40 | `The program [${program.name}] ` + 41 | `at address [${program.address.toString()}] ` + 42 | `raised an error${ofCode} ` + 43 | `that translates to "${cause.message}".`; 44 | super(message, program, cause, logs); 45 | } 46 | } 47 | 48 | /** @group Errors */ 49 | export class UnknownProgramError extends ProgramError { 50 | readonly name: string = 'UnknownProgramError'; 51 | constructor(program: Program, cause: UnderlyingProgramError) { 52 | const ofCode = cause.code ? ` of code [${cause.code}]` : ''; 53 | const message = 54 | `The program [${program.name}] ` + 55 | `at address [${program.address.toString()}] ` + 56 | `raised an error${ofCode} ` + 57 | `that is not recognized by the programs registered on the SDK. ` + 58 | `Please check the underlying program error below for more details.`; 59 | super(message, program, cause, cause.logs); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/js/src/errors/ReadApiError.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexError } from './MetaplexError'; 2 | 3 | /** @group Errors */ 4 | export class ReadApiError extends MetaplexError { 5 | readonly name: string = 'ReadApiError'; 6 | constructor(message: string, cause?: Error) { 7 | super(message, 'rpc', undefined, cause); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/js/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IrysError'; 2 | export * from './MetaplexError'; 3 | export * from './ProgramError'; 4 | export * from './RpcError'; 5 | export * from './SdkError'; 6 | export * from './ReadApiError'; 7 | -------------------------------------------------------------------------------- /packages/js/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export * from './plugins'; 3 | export * from './types'; 4 | export * from './utils'; 5 | export * from './Metaplex'; 6 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/accounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Auctioneer, 3 | AuctionHouse, 4 | ListingReceipt, 5 | BidReceipt, 6 | PurchaseReceipt, 7 | } from '@metaplex-foundation/mpl-auction-house'; 8 | import { 9 | Account, 10 | getAccountParsingAndAssertingFunction, 11 | getAccountParsingFunction, 12 | } from '@/types'; 13 | 14 | /** @group Accounts */ 15 | export type AuctioneerAccount = Account; 16 | 17 | /** @group Account Helpers */ 18 | export const parseAuctioneerAccount = getAccountParsingFunction(Auctioneer); 19 | 20 | /** @group Account Helpers */ 21 | export const toAuctioneerAccount = 22 | getAccountParsingAndAssertingFunction(Auctioneer); 23 | 24 | /** @group Accounts */ 25 | export type AuctionHouseAccount = Account; 26 | 27 | /** @group Account Helpers */ 28 | export const parseAuctionHouseAccount = getAccountParsingFunction(AuctionHouse); 29 | 30 | /** @group Account Helpers */ 31 | export const toAuctionHouseAccount = 32 | getAccountParsingAndAssertingFunction(AuctionHouse); 33 | 34 | /** @group Accounts */ 35 | export type ListingReceiptAccount = Account; 36 | 37 | /** @group Account Helpers */ 38 | export const parseListingReceiptAccount = 39 | getAccountParsingFunction(ListingReceipt); 40 | 41 | /** @group Account Helpers */ 42 | export const toListingReceiptAccount = 43 | getAccountParsingAndAssertingFunction(ListingReceipt); 44 | 45 | /** @group Accounts */ 46 | export type BidReceiptAccount = Account; 47 | 48 | /** @group Account Helpers */ 49 | export const parseBidReceiptAccount = getAccountParsingFunction(BidReceipt); 50 | 51 | /** @group Account Helpers */ 52 | export const toBidReceiptAccount = 53 | getAccountParsingAndAssertingFunction(BidReceipt); 54 | 55 | /** @group Accounts */ 56 | export type PurchaseReceiptAccount = Account; 57 | 58 | /** @group Account Helpers */ 59 | export const parsePurchaseReceiptAccount = 60 | getAccountParsingFunction(PurchaseReceipt); 61 | 62 | /** @group Account Helpers */ 63 | export const toPurchaseReceiptAccount = 64 | getAccountParsingAndAssertingFunction(PurchaseReceipt); 65 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/constants.ts: -------------------------------------------------------------------------------- 1 | import { AuthorityScope } from '@metaplex-foundation/mpl-auction-house'; 2 | import { toBigNumber } from '@/types'; 3 | 4 | // Auctioneer uses "u64::MAX" for the price which is "2^64 − 1". 5 | export const AUCTIONEER_PRICE = toBigNumber('18446744073709551615'); 6 | 7 | export const AUCTIONEER_ALL_SCOPES = [ 8 | AuthorityScope.Deposit, 9 | AuthorityScope.Buy, 10 | AuthorityScope.PublicBuy, 11 | AuthorityScope.ExecuteSale, 12 | AuthorityScope.Sell, 13 | AuthorityScope.Cancel, 14 | AuthorityScope.Withdraw, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/gpaBuilders/BidReceiptGpaBuilder.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { GpaBuilder } from '@/utils'; 3 | 4 | type AccountDiscriminator = [ 5 | number, 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number 13 | ]; 14 | // TODO: copied from auction house SDK 15 | // SDK should either provide a GPA builder or expose this discriminator 16 | const bidReceiptDiscriminator: AccountDiscriminator = [ 17 | 186, 150, 141, 135, 59, 122, 39, 99, 18 | ]; 19 | 20 | const PUBLIC_KEY_LENGTH = PublicKey.default.toBytes().byteLength; 21 | 22 | const TRADE_STATE = bidReceiptDiscriminator.length; 23 | const BOOKKEEPER = TRADE_STATE + PUBLIC_KEY_LENGTH; 24 | const AUCTION_HOUSE = BOOKKEEPER + PUBLIC_KEY_LENGTH; 25 | const BUYER = AUCTION_HOUSE + PUBLIC_KEY_LENGTH; 26 | const METADATA = BUYER + PUBLIC_KEY_LENGTH; 27 | 28 | export class BidReceiptGpaBuilder extends GpaBuilder { 29 | whereDiscriminator(discrimator: AccountDiscriminator) { 30 | return this.where(0, Buffer.from(discrimator)); 31 | } 32 | 33 | bidReceiptAccounts() { 34 | return this.whereDiscriminator(bidReceiptDiscriminator); 35 | } 36 | 37 | whereAuctionHouse(auctionHouseAddress: PublicKey) { 38 | return this.bidReceiptAccounts().where(AUCTION_HOUSE, auctionHouseAddress); 39 | } 40 | 41 | whereBuyer(buyerAddress: PublicKey) { 42 | return this.where(BUYER, buyerAddress); 43 | } 44 | 45 | whereMetadata(metadataAddress: PublicKey) { 46 | return this.where(METADATA, metadataAddress); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/gpaBuilders/ListingReceiptGpaBuilder.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { GpaBuilder } from '@/utils'; 3 | 4 | type AccountDiscriminator = [ 5 | number, 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number 13 | ]; 14 | // TODO: copied from auction house SDK 15 | // SDK should either provide a GPA builder or expose this discriminator 16 | const listingReceiptDiscriminator: AccountDiscriminator = [ 17 | 240, 71, 225, 94, 200, 75, 84, 231, 18 | ]; 19 | 20 | const PUBLIC_KEY_LENGTH = PublicKey.default.toBytes().byteLength; 21 | 22 | const TRADE_STATE = listingReceiptDiscriminator.length; 23 | const BOOKKEEPER = TRADE_STATE + PUBLIC_KEY_LENGTH; 24 | const AUCTION_HOUSE = BOOKKEEPER + PUBLIC_KEY_LENGTH; 25 | const SELLER = AUCTION_HOUSE + PUBLIC_KEY_LENGTH; 26 | const METADATA = SELLER + PUBLIC_KEY_LENGTH; 27 | 28 | export class ListingReceiptGpaBuilder extends GpaBuilder { 29 | whereDiscriminator(discrimator: AccountDiscriminator) { 30 | return this.where(0, Buffer.from(discrimator)); 31 | } 32 | 33 | listingReceiptAccounts() { 34 | return this.whereDiscriminator(listingReceiptDiscriminator); 35 | } 36 | 37 | whereAuctionHouse(auctionHouseAddress: PublicKey) { 38 | return this.listingReceiptAccounts().where( 39 | AUCTION_HOUSE, 40 | auctionHouseAddress 41 | ); 42 | } 43 | 44 | whereSeller(sellerAddress: PublicKey) { 45 | return this.where(SELLER, sellerAddress); 46 | } 47 | 48 | whereMetadata(metadataAddress: PublicKey) { 49 | return this.where(METADATA, metadataAddress); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/gpaBuilders/PurchaseReceiptGpaBuilder.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { GpaBuilder } from '@/utils'; 3 | 4 | type AccountDiscriminator = [ 5 | number, 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number 13 | ]; 14 | // TODO: copied from auction house SDK 15 | // SDK should either provide a GPA builder or expose this discriminator 16 | const purchaseReceiptDiscriminator: AccountDiscriminator = [ 17 | 79, 127, 222, 137, 154, 131, 150, 134, 18 | ]; 19 | 20 | const PUBLIC_KEY_LENGTH = PublicKey.default.toBytes().byteLength; 21 | 22 | const BOOKKEEPER = purchaseReceiptDiscriminator.length; 23 | const BUYER = BOOKKEEPER + PUBLIC_KEY_LENGTH; 24 | const SELLER = BUYER + PUBLIC_KEY_LENGTH; 25 | const AUCTION_HOUSE = SELLER + PUBLIC_KEY_LENGTH; 26 | const METADATA = AUCTION_HOUSE + PUBLIC_KEY_LENGTH; 27 | 28 | export class PurchaseReceiptGpaBuilder extends GpaBuilder { 29 | whereDiscriminator(discrimator: AccountDiscriminator) { 30 | return this.where(0, Buffer.from(discrimator)); 31 | } 32 | 33 | purchaseReceiptAccounts() { 34 | return this.whereDiscriminator(purchaseReceiptDiscriminator); 35 | } 36 | 37 | whereAuctionHouse(auctionHouseAddress: PublicKey) { 38 | return this.purchaseReceiptAccounts().where( 39 | AUCTION_HOUSE, 40 | auctionHouseAddress 41 | ); 42 | } 43 | 44 | whereBuyer(buyerAddress: PublicKey) { 45 | return this.where(BUYER, buyerAddress); 46 | } 47 | 48 | whereSeller(sellerAddress: PublicKey) { 49 | return this.where(SELLER, sellerAddress); 50 | } 51 | 52 | whereMetadata(metadataAddress: PublicKey) { 53 | return this.where(METADATA, metadataAddress); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/gpaBuilders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BidReceiptGpaBuilder'; 2 | export * from './ListingReceiptGpaBuilder'; 3 | export * from './PurchaseReceiptGpaBuilder'; 4 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts'; 2 | export * from './AuctionHouseBuildersClient'; 3 | export * from './AuctionHouseClient'; 4 | export * from './AuctionHousePdasClient'; 5 | export * from './errors'; 6 | export * from './plugin'; 7 | export * from './operations'; 8 | export * from './models'; 9 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuctionHouse'; 2 | export * from './Bid'; 3 | export * from './Listing'; 4 | export * from './Purchase'; 5 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/operations/findBidByTradeState.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import { AuctionHouse, Bid } from '../models'; 3 | import type { Metaplex } from '@/Metaplex'; 4 | import { 5 | Operation, 6 | OperationHandler, 7 | OperationScope, 8 | useOperation, 9 | } from '@/types'; 10 | 11 | // ----------------- 12 | // Operation 13 | // ----------------- 14 | 15 | const Key = 'FindBidByTradeStateOperation' as const; 16 | 17 | /** 18 | * Finds a Bid by its trade state address. 19 | * 20 | * ```ts 21 | * const nft = await metaplex 22 | * .auctionHouse() 23 | * .findBidByTradeState({ tradeStateAddress, auctionHouse }; 24 | * ``` 25 | * 26 | * @group Operations 27 | * @category Constructors 28 | */ 29 | export const findBidByTradeStateOperation = 30 | useOperation(Key); 31 | 32 | /** 33 | * @group Operations 34 | * @category Types 35 | */ 36 | export type FindBidByTradeStateOperation = Operation< 37 | typeof Key, 38 | FindBidByTradeStateInput, 39 | Bid 40 | >; 41 | 42 | /** 43 | * @group Operations 44 | * @category Inputs 45 | */ 46 | export type FindBidByTradeStateInput = { 47 | /** Buyer trade state PDA account encoding the bid order. */ 48 | tradeStateAddress: PublicKey; 49 | 50 | /** A model of the Auction House related to this bid. */ 51 | auctionHouse: AuctionHouse; 52 | 53 | /** 54 | * Whether or not we should fetch the JSON Metadata for the NFT or SFT. 55 | * 56 | * @defaultValue `true` 57 | */ 58 | loadJsonMetadata?: boolean; 59 | }; 60 | 61 | /** 62 | * @group Operations 63 | * @category Handlers 64 | */ 65 | export const findBidByTradeStateOperationHandler: OperationHandler = 66 | { 67 | handle: async ( 68 | operation: FindBidByTradeStateOperation, 69 | metaplex: Metaplex, 70 | scope: OperationScope 71 | ) => { 72 | const { tradeStateAddress } = operation.input; 73 | const receiptAddress = metaplex.auctionHouse().pdas().bidReceipt({ 74 | tradeState: tradeStateAddress, 75 | programs: scope.programs, 76 | }); 77 | 78 | return metaplex 79 | .auctionHouse() 80 | .findBidByReceipt({ receiptAddress, ...operation.input }, scope); 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/operations/getBuyerBalance.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { 4 | Operation, 5 | OperationHandler, 6 | OperationScope, 7 | SolAmount, 8 | useOperation, 9 | } from '@/types'; 10 | 11 | // ----------------- 12 | // Operation 13 | // ----------------- 14 | 15 | const Key = 'GetBuyerBalanceOperation' as const; 16 | 17 | /** 18 | * Gets the balance of a buyer's escrow account for a given Auction House. 19 | * 20 | * ```ts 21 | * await metaplex 22 | * .auctionHouse() 23 | * .getBuyerBalance({ auctionHouse, buyerAddress }; 24 | * ``` 25 | * 26 | * @group Operations 27 | * @category Constructors 28 | */ 29 | export const getBuyerBalanceOperation = 30 | useOperation(Key); 31 | 32 | /** 33 | * @group Operations 34 | * @category Types 35 | */ 36 | export type GetBuyerBalanceOperation = Operation< 37 | typeof Key, 38 | GetBuyerBalanceInput, 39 | GetBuyerBalanceOutput 40 | >; 41 | 42 | /** 43 | * @group Operations 44 | * @category Inputs 45 | */ 46 | export type GetBuyerBalanceInput = { 47 | /** The Auction House in which to get the buyer's escrow balance. */ 48 | auctionHouse: PublicKey; 49 | 50 | /** The buyer's address. */ 51 | buyerAddress: PublicKey; 52 | }; 53 | 54 | /** 55 | * @group Operations 56 | * @category Outputs 57 | */ 58 | export type GetBuyerBalanceOutput = SolAmount; 59 | 60 | /** 61 | * @group Operations 62 | * @category Handlers 63 | */ 64 | export const getBuyerBalanceOperationHandler: OperationHandler = 65 | { 66 | handle: async ( 67 | operation: GetBuyerBalanceOperation, 68 | metaplex: Metaplex, 69 | scope: OperationScope 70 | ) => { 71 | const { auctionHouse, buyerAddress } = operation.input; 72 | const buyerEscrow = metaplex.auctionHouse().pdas().buyerEscrow({ 73 | auctionHouse, 74 | buyer: buyerAddress, 75 | programs: scope.programs, 76 | }); 77 | 78 | return metaplex.rpc().getBalance(buyerEscrow, scope.commitment); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cancelBid'; 2 | export * from './cancelListing'; 3 | export * from './createAuctionHouse'; 4 | export * from './createBid'; 5 | export * from './createListing'; 6 | export * from './depositToBuyerAccount'; 7 | export * from './directBuy'; 8 | export * from './directSell'; 9 | export * from './executeSale'; 10 | export * from './findAuctionHouseByAddress'; 11 | export * from './findAuctionHouseByCreatorAndMint'; 12 | export * from './findBidByReceipt'; 13 | export * from './findBidByTradeState'; 14 | export * from './findBids'; 15 | export * from './findListingByReceipt'; 16 | export * from './findListingByTradeState'; 17 | export * from './findListings'; 18 | export * from './findPurchaseByReceipt'; 19 | export * from './findPurchaseByTradeState'; 20 | export * from './findPurchases'; 21 | export * from './getBuyerBalance'; 22 | export * from './loadBid'; 23 | export * from './loadListing'; 24 | export * from './loadPurchase'; 25 | export * from './updateAuctionHouse'; 26 | export * from './withdrawFromBuyerAccount'; 27 | export * from './withdrawFromFeeAccount'; 28 | export * from './withdrawFromTreasuryAccount'; 29 | -------------------------------------------------------------------------------- /packages/js/src/plugins/auctionHouseModule/operations/loadListing.ts: -------------------------------------------------------------------------------- 1 | import { assertNftOrSftWithToken } from '../../nftModule'; 2 | import { LazyListing, Listing } from '../models/Listing'; 3 | import type { Metaplex } from '@/Metaplex'; 4 | import { 5 | amount, 6 | Operation, 7 | OperationHandler, 8 | OperationScope, 9 | useOperation, 10 | } from '@/types'; 11 | 12 | // ----------------- 13 | // Operation 14 | // ----------------- 15 | 16 | const Key = 'LoadListingOperation' as const; 17 | 18 | /** 19 | * Transforms a `LazyListing` model into a `Listing` model. 20 | * 21 | * ```ts 22 | * const listing = await metaplex 23 | * .auctionHouse() 24 | * .loadListing({ lazyListing }; 25 | * ``` 26 | * 27 | * @group Operations 28 | * @category Constructors 29 | */ 30 | export const loadListingOperation = useOperation(Key); 31 | 32 | /** 33 | * @group Operations 34 | * @category Types 35 | */ 36 | export type LoadListingOperation = Operation< 37 | typeof Key, 38 | LoadListingInput, 39 | Listing 40 | >; 41 | 42 | /** 43 | * @group Operations 44 | * @category Inputs 45 | */ 46 | export type LoadListingInput = { 47 | /** The `LazyListing` model to transform into the `Listing`. */ 48 | lazyListing: LazyListing; 49 | 50 | /** 51 | * Whether or not we should fetch the JSON Metadata for the NFT or SFT. 52 | * 53 | * @defaultValue `true` 54 | */ 55 | loadJsonMetadata?: boolean; 56 | }; 57 | 58 | /** 59 | * @group Operations 60 | * @category Handlers 61 | */ 62 | export const loadListingOperationHandler: OperationHandler = 63 | { 64 | handle: async ( 65 | operation: LoadListingOperation, 66 | metaplex: Metaplex, 67 | scope: OperationScope 68 | ) => { 69 | const { lazyListing, loadJsonMetadata = true } = operation.input; 70 | const asset = await metaplex.nfts().findByMetadata( 71 | { 72 | metadata: lazyListing.metadataAddress, 73 | tokenOwner: lazyListing.sellerAddress, 74 | loadJsonMetadata, 75 | }, 76 | scope 77 | ); 78 | assertNftOrSftWithToken(asset); 79 | 80 | return { 81 | ...lazyListing, 82 | model: 'listing', 83 | lazy: false, 84 | asset, 85 | tokens: amount(lazyListing.tokens, asset.mint.currency), 86 | }; 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/asserts.ts: -------------------------------------------------------------------------------- 1 | import { CandyMachine, CandyMachineItem } from './models'; 2 | import { 3 | CandyMachineItemTextTooLongError, 4 | CandyMachineCannotAddAmountError, 5 | CandyMachineIsFullError, 6 | } from './errors'; 7 | 8 | export const assertNotFull = ( 9 | candyMachine: Pick, 10 | index: number 11 | ) => { 12 | if (candyMachine.itemsAvailable.lten(candyMachine.itemsLoaded)) { 13 | throw new CandyMachineIsFullError( 14 | index, 15 | candyMachine.itemsAvailable.toNumber() 16 | ); 17 | } 18 | }; 19 | 20 | export const assertCanAdd = ( 21 | candyMachine: Pick, 22 | index: number, 23 | amount: number 24 | ) => { 25 | if (index + amount > candyMachine.itemsAvailable.toNumber()) { 26 | throw new CandyMachineCannotAddAmountError( 27 | index, 28 | amount, 29 | candyMachine.itemsAvailable.toNumber() 30 | ); 31 | } 32 | }; 33 | 34 | export const assertAllItemConstraints = ( 35 | candyMachine: Pick, 36 | items: Pick[] 37 | ) => { 38 | if (candyMachine.itemSettings.type !== 'configLines') { 39 | return; 40 | } 41 | 42 | const { nameLength } = candyMachine.itemSettings; 43 | const { uriLength } = candyMachine.itemSettings; 44 | 45 | for (let i = 0; i < items.length; i++) { 46 | if (items[i].name.length > nameLength) { 47 | throw new CandyMachineItemTextTooLongError( 48 | i, 49 | 'name', 50 | items[i].name, 51 | nameLength 52 | ); 53 | } 54 | if (items[i].uri.length > uriLength) { 55 | throw new CandyMachineItemTextTooLongError( 56 | i, 57 | 'uri', 58 | items[i].uri, 59 | uriLength 60 | ); 61 | } 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_NAME_LENGTH = 32; 2 | export const MAX_SYMBOL_LENGTH = 10; 3 | export const MAX_URI_LENGTH = 200; 4 | export const MAX_CREATOR_LIMIT = 5; 5 | export const MAX_CREATOR_LEN = 32 + 1 + 1; 6 | export const CONFIG_LINE_SIZE = 4 + MAX_NAME_LENGTH + 4 + MAX_URI_LENGTH; 7 | 8 | export const CANDY_MACHINE_HIDDEN_SECTION = 9 | 8 + // discriminator 10 | 8 + // features 11 | 32 + // authority 12 | 32 + // mint authority 13 | 32 + // collection mint 14 | 8 + // items redeemed 15 | 8 + // items available (config data) 16 | 4 + 17 | MAX_SYMBOL_LENGTH + // u32 + max symbol length 18 | 2 + // seller fee basis points 19 | 8 + // max supply 20 | 1 + // is mutable 21 | 4 + 22 | MAX_CREATOR_LIMIT * MAX_CREATOR_LEN + // u32 + creators vec 23 | 1 + // option (config lines settings) 24 | 4 + 25 | MAX_NAME_LENGTH + // u32 + max name length 26 | 4 + // name length 27 | 4 + 28 | MAX_URI_LENGTH + // u32 + max uri length 29 | 4 + // uri length 30 | 1 + // is sequential 31 | 1 + // option (hidden setting) 32 | 4 + 33 | MAX_NAME_LENGTH + // u32 + max name length 34 | 4 + 35 | MAX_URI_LENGTH + // u32 + max uri length 36 | 32; // hash 37 | 38 | export const CANDY_GUARD_LABEL_SIZE = 6; 39 | export const CANDY_GUARD_DATA = 40 | 8 + // discriminator 41 | 32 + // base 42 | 1 + // bump 43 | 32; // authority 44 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/addressGate.ts: -------------------------------------------------------------------------------- 1 | import { addressGateBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { createSerializerFromBeet, PublicKey } from '@/types'; 4 | 5 | /** 6 | * The addressGate guard restricts the mint to a single 7 | * address which must match the minting wallet's address. 8 | * 9 | * This object defines the settings that should be 10 | * provided when creating and/or updating a Candy 11 | * Machine if you wish to enable this guard. 12 | */ 13 | export type AddressGateGuardSettings = { 14 | /** The only address that is allowed to mint from the Candy Machine. */ 15 | address: PublicKey; 16 | }; 17 | 18 | /** @internal */ 19 | export const addressGateGuardManifest: CandyGuardManifest = 20 | { 21 | name: 'addressGate', 22 | settingsBytes: 32, 23 | settingsSerializer: createSerializerFromBeet(addressGateBeet), 24 | }; 25 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/botTax.ts: -------------------------------------------------------------------------------- 1 | import { BotTax, botTaxBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { 4 | createSerializerFromBeet, 5 | lamports, 6 | mapSerializer, 7 | SolAmount, 8 | } from '@/types'; 9 | 10 | /** 11 | * The botTax guard charges a penalty for invalid transactions 12 | * in order to discourage bots from attempting to mint NFTs. 13 | * 14 | * This bot tax works in combinaison with other guards and 15 | * will trigger whenever a minting wallet attempts to mint 16 | * an NFT such that other guards would have rejected the mint. 17 | * 18 | * For example, if you have a startDate guard and a botTax guard, 19 | * anyone trying to mint before the defined start date will be 20 | * charged the bot tax instead of receiving a specific startDate error. 21 | * 22 | * This object defines the settings that should be 23 | * provided when creating and/or updating a Candy 24 | * Machine if you wish to enable this guard. 25 | */ 26 | export type BotTaxGuardSettings = { 27 | /** The amount in SOL to charge for an invalid transaction. */ 28 | lamports: SolAmount; 29 | 30 | /** 31 | * Whether or not we should charge the bot tax when a mint instruction 32 | * is not the last instruction of the transaction. 33 | * 34 | * This is useful if you want to prevent bots from adding extra instructions 35 | * after minting to detect if a bot tax was charged and, in this case, 36 | * throw an error to make the transaction fail and avoid the bot tax. 37 | */ 38 | lastInstruction: boolean; 39 | }; 40 | 41 | /** @internal */ 42 | export const botTaxGuardManifest: CandyGuardManifest = { 43 | name: 'botTax', 44 | settingsBytes: 9, 45 | settingsSerializer: mapSerializer( 46 | createSerializerFromBeet(botTaxBeet), 47 | (settings) => ({ ...settings, lamports: lamports(settings.lamports) }), 48 | (settings) => ({ ...settings, lamports: settings.lamports.basisPoints }) 49 | ), 50 | }; 51 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/endDate.ts: -------------------------------------------------------------------------------- 1 | import { EndDate, endDateBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { 4 | createSerializerFromBeet, 5 | DateTime, 6 | mapSerializer, 7 | toDateTime, 8 | } from '@/types'; 9 | 10 | /** 11 | * The endDate guard is used to specify a date to end the mint. 12 | * Any transaction received after the end date will fail. 13 | * 14 | * This object defines the settings that should be 15 | * provided when creating and/or updating a Candy 16 | * Machine if you wish to enable this guard. 17 | */ 18 | export type EndDateGuardSettings = { 19 | /** The date after which minting is no longer possible. */ 20 | date: DateTime; 21 | }; 22 | 23 | /** @internal */ 24 | export const endDateGuardManifest: CandyGuardManifest = { 25 | name: 'endDate', 26 | settingsBytes: 8, 27 | settingsSerializer: mapSerializer( 28 | createSerializerFromBeet(endDateBeet), 29 | (settings) => ({ date: toDateTime(settings.date) }), 30 | (settings) => settings 31 | ), 32 | }; 33 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addressGate'; 2 | export * from './allowList'; 3 | export * from './botTax'; 4 | export * from './core'; 5 | export * from './default'; 6 | export * from './endDate'; 7 | export * from './freezeSolPayment'; 8 | export * from './freezeTokenPayment'; 9 | export * from './gatekeeper'; 10 | export * from './mintLimit'; 11 | export * from './nftBurn'; 12 | export * from './nftGate'; 13 | export * from './nftPayment'; 14 | export * from './programGate'; 15 | export * from './redeemedAmount'; 16 | export * from './solPayment'; 17 | export * from './startDate'; 18 | export * from './thirdPartySigner'; 19 | export * from './tokenBurn'; 20 | export * from './tokenGate'; 21 | export * from './tokenPayment'; 22 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/mintLimit.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { mintLimitBeet } from '@metaplex-foundation/mpl-candy-guard'; 3 | import { CandyGuardManifest } from './core'; 4 | import { createSerializerFromBeet } from '@/types'; 5 | 6 | /** 7 | * The mintLimit guard allows to specify a limit on the 8 | * number of mints for each individual wallet. 9 | * 10 | * The limit is set per wallet, per candy machine and per 11 | * identified (provided in the settings) to allow multiple 12 | * mint limits within a Candy Machine. This is particularly 13 | * useful when using groups of guards and we want each of them 14 | * to have a different mint limit. 15 | * 16 | * This object defines the settings that should be 17 | * provided when creating and/or updating a Candy 18 | * Machine if you wish to enable this guard. 19 | */ 20 | export type MintLimitGuardSettings = { 21 | /** 22 | * A unique identitifer for the limit 23 | * for a given wallet and candy machine. 24 | */ 25 | id: number; 26 | 27 | /** The maximum number of mints allowed. */ 28 | limit: number; 29 | }; 30 | 31 | /** @internal */ 32 | export const mintLimitGuardManifest: CandyGuardManifest = 33 | { 34 | name: 'mintLimit', 35 | settingsBytes: 3, 36 | settingsSerializer: createSerializerFromBeet(mintLimitBeet), 37 | mintSettingsParser: ({ 38 | metaplex, 39 | settings, 40 | payer, 41 | candyMachine, 42 | candyGuard, 43 | programs, 44 | }) => { 45 | const counterPda = metaplex.candyMachines().pdas().mintLimitCounter({ 46 | id: settings.id, 47 | user: payer.publicKey, 48 | candyMachine, 49 | candyGuard, 50 | programs, 51 | }); 52 | 53 | return { 54 | arguments: Buffer.from([]), 55 | remainingAccounts: [ 56 | { 57 | address: counterPda, 58 | isSigner: false, 59 | isWritable: true, 60 | }, 61 | ], 62 | }; 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/redeemedAmount.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RedeemedAmount, 3 | redeemedAmountBeet, 4 | } from '@metaplex-foundation/mpl-candy-guard'; 5 | import { CandyGuardManifest } from './core'; 6 | import { 7 | BigNumber, 8 | createSerializerFromBeet, 9 | mapSerializer, 10 | toBigNumber, 11 | } from '@/types'; 12 | 13 | /** 14 | * The redeemedAmount guard forbids minting when the 15 | * number of minted NFTs for the entire Candy Machine 16 | * reaches the configured maximum amount. 17 | * 18 | * This object defines the settings that should be 19 | * provided when creating and/or updating a Candy 20 | * Machine if you wish to enable this guard. 21 | */ 22 | export type RedeemedAmountGuardSettings = { 23 | /** The maximum amount of NFTs that can be minted using that guard. */ 24 | maximum: BigNumber; 25 | }; 26 | 27 | /** @internal */ 28 | export const redeemedAmountGuardManifest: CandyGuardManifest = 29 | { 30 | name: 'redeemedAmount', 31 | settingsBytes: 8, 32 | settingsSerializer: mapSerializer< 33 | RedeemedAmount, 34 | RedeemedAmountGuardSettings 35 | >( 36 | createSerializerFromBeet(redeemedAmountBeet), 37 | (settings) => ({ maximum: toBigNumber(settings.maximum) }), 38 | (settings) => settings 39 | ), 40 | }; 41 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/solPayment.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { 3 | SolPayment, 4 | solPaymentBeet, 5 | } from '@metaplex-foundation/mpl-candy-guard'; 6 | import { CandyGuardManifest } from './core'; 7 | import { 8 | createSerializerFromBeet, 9 | lamports, 10 | mapSerializer, 11 | PublicKey, 12 | SolAmount, 13 | } from '@/types'; 14 | 15 | /** 16 | * The solPayment guard is used to charge an 17 | * amount in SOL for the minted NFT. 18 | * 19 | * This object defines the settings that should be 20 | * provided when creating and/or updating a Candy 21 | * Machine if you wish to enable this guard. 22 | */ 23 | export type SolPaymentGuardSettings = { 24 | /** The amount in SOL to charge for. */ 25 | amount: SolAmount; 26 | 27 | /** The configured destination address to send the funds to. */ 28 | destination: PublicKey; 29 | }; 30 | 31 | /** @internal */ 32 | export const solPaymentGuardManifest: CandyGuardManifest = 33 | { 34 | name: 'solPayment', 35 | settingsBytes: 40, 36 | settingsSerializer: mapSerializer( 37 | createSerializerFromBeet(solPaymentBeet), 38 | (settings) => ({ ...settings, amount: lamports(settings.lamports) }), 39 | (settings) => ({ ...settings, lamports: settings.amount.basisPoints }) 40 | ), 41 | mintSettingsParser: ({ settings }) => { 42 | return { 43 | arguments: Buffer.from([]), 44 | remainingAccounts: [ 45 | { 46 | isSigner: false, 47 | address: settings.destination, 48 | isWritable: true, 49 | }, 50 | ], 51 | }; 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/startDate.ts: -------------------------------------------------------------------------------- 1 | import { StartDate, startDateBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { 4 | createSerializerFromBeet, 5 | DateTime, 6 | mapSerializer, 7 | toDateTime, 8 | } from '@/types'; 9 | 10 | /** 11 | * The startDate guard determines the start date of the mint. 12 | * Before this date, minting is not allowed. 13 | * 14 | * This object defines the settings that should be 15 | * provided when creating and/or updating a Candy 16 | * Machine if you wish to enable this guard. 17 | */ 18 | export type StartDateGuardSettings = { 19 | /** The date before which minting is not yet possible. */ 20 | date: DateTime; 21 | }; 22 | 23 | /** @internal */ 24 | export const startDateGuardManifest: CandyGuardManifest = 25 | { 26 | name: 'startDate', 27 | settingsBytes: 8, 28 | settingsSerializer: mapSerializer( 29 | createSerializerFromBeet(startDateBeet), 30 | (settings) => ({ date: toDateTime(settings.date) }), 31 | (settings) => settings 32 | ), 33 | }; 34 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/thirdPartySigner.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { thirdPartySignerBeet } from '@metaplex-foundation/mpl-candy-guard'; 3 | import { GuardMintSettingsMissingError } from '../errors'; 4 | import { CandyGuardManifest } from './core'; 5 | import { createSerializerFromBeet, PublicKey, Signer } from '@/types'; 6 | 7 | /** 8 | * The thirdPartySigner guard requires a predefined 9 | * address to sign the mint transaction. The signer will need 10 | * to be passed within the mint settings of this guard. 11 | * 12 | * This allows for more centralized mints where every single 13 | * mint transaction has to go through a specific signer. 14 | * 15 | * This object defines the settings that should be 16 | * provided when creating and/or updating a Candy 17 | * Machine if you wish to enable this guard. 18 | * 19 | * @see {@link ThirdPartySignerGuardMintSettings} for more 20 | * information on the mint settings of this guard. 21 | */ 22 | export type ThirdPartySignerGuardSettings = { 23 | /** 24 | * The address of the signer that will 25 | * need to sign each mint transaction. 26 | */ 27 | signerKey: PublicKey; 28 | }; 29 | 30 | /** 31 | * The settings for the thirdPartySigner guard that could 32 | * be provided when minting from the Candy Machine. 33 | * 34 | * @see {@link ThirdPartySignerGuardSettings} for more 35 | * information on the thirdPartySigner guard itself. 36 | */ 37 | export type ThirdPartySignerGuardMintSettings = { 38 | /** The required third party signer. */ 39 | signer: Signer; 40 | }; 41 | 42 | /** @internal */ 43 | export const thirdPartySignerGuardManifest: CandyGuardManifest< 44 | ThirdPartySignerGuardSettings, 45 | ThirdPartySignerGuardMintSettings 46 | > = { 47 | name: 'thirdPartySigner', 48 | settingsBytes: 32, 49 | settingsSerializer: createSerializerFromBeet(thirdPartySignerBeet), 50 | mintSettingsParser: ({ mintSettings }) => { 51 | if (!mintSettings) { 52 | throw new GuardMintSettingsMissingError('thirdPartySigner'); 53 | } 54 | 55 | return { 56 | arguments: Buffer.from([]), 57 | remainingAccounts: [ 58 | { 59 | isSigner: true, 60 | address: mintSettings.signer, 61 | isWritable: true, 62 | }, 63 | ], 64 | }; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/tokenBurn.ts: -------------------------------------------------------------------------------- 1 | import { TokenBurn, tokenBurnBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { 4 | createSerializerFromBeet, 5 | mapSerializer, 6 | PublicKey, 7 | SplTokenAmount, 8 | token, 9 | } from '@/types'; 10 | 11 | /** 12 | * The tokenBurn guard restricts minting to token holders 13 | * of a specified mint account and burns the holder's tokens 14 | * when minting. The `amount` determines how many tokens are required. 15 | * 16 | * This guard alone does not limit how many times a holder 17 | * can mint. A holder can mint as many times as they have 18 | * the required amount of tokens to burn. 19 | * 20 | * This object defines the settings that should be 21 | * provided when creating and/or updating a Candy 22 | * Machine if you wish to enable this guard. 23 | */ 24 | export type TokenBurnGuardSettings = { 25 | /** The mint address of the required tokens. */ 26 | mint: PublicKey; 27 | 28 | /** The amount of tokens required to mint an NFT. */ 29 | amount: SplTokenAmount; 30 | }; 31 | 32 | /** @internal */ 33 | export const tokenBurnGuardManifest: CandyGuardManifest = 34 | { 35 | name: 'tokenBurn', 36 | settingsBytes: 40, 37 | settingsSerializer: mapSerializer( 38 | createSerializerFromBeet(tokenBurnBeet), 39 | (settings) => ({ ...settings, amount: token(settings.amount) }), 40 | (settings) => ({ ...settings, amount: settings.amount.basisPoints }) 41 | ), 42 | mintSettingsParser: ({ metaplex, settings, payer, programs }) => { 43 | const tokenAccount = metaplex.tokens().pdas().associatedTokenAccount({ 44 | mint: settings.mint, 45 | owner: payer.publicKey, 46 | programs, 47 | }); 48 | 49 | return { 50 | arguments: Buffer.from([]), 51 | remainingAccounts: [ 52 | { 53 | isSigner: false, 54 | address: tokenAccount, 55 | isWritable: true, 56 | }, 57 | { 58 | isSigner: false, 59 | address: settings.mint, 60 | isWritable: true, 61 | }, 62 | ], 63 | }; 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/guards/tokenGate.ts: -------------------------------------------------------------------------------- 1 | import { TokenGate, tokenGateBeet } from '@metaplex-foundation/mpl-candy-guard'; 2 | import { CandyGuardManifest } from './core'; 3 | import { 4 | createSerializerFromBeet, 5 | mapSerializer, 6 | PublicKey, 7 | SplTokenAmount, 8 | token, 9 | } from '@/types'; 10 | 11 | /** 12 | * The tokenGate guard restricts minting to token holders 13 | * of a specified mint account. The `amount` determines 14 | * how many tokens are required. 15 | * 16 | * This object defines the settings that should be 17 | * provided when creating and/or updating a Candy 18 | * Machine if you wish to enable this guard. 19 | */ 20 | export type TokenGateGuardSettings = { 21 | /** The mint address of the required tokens. */ 22 | mint: PublicKey; 23 | 24 | /** The amount of tokens required to mint an NFT. */ 25 | amount: SplTokenAmount; 26 | }; 27 | 28 | /** @internal */ 29 | export const tokenGateGuardManifest: CandyGuardManifest = 30 | { 31 | name: 'tokenGate', 32 | settingsBytes: 40, 33 | settingsSerializer: mapSerializer( 34 | createSerializerFromBeet(tokenGateBeet), 35 | (settings) => ({ ...settings, amount: token(settings.amount) }), 36 | (settings) => ({ ...settings, amount: settings.amount.basisPoints }) 37 | ), 38 | mintSettingsParser: ({ metaplex, settings, payer, programs }) => { 39 | const tokenAccount = metaplex.tokens().pdas().associatedTokenAccount({ 40 | mint: settings.mint, 41 | owner: payer.publicKey, 42 | programs, 43 | }); 44 | 45 | return { 46 | arguments: Buffer.from([]), 47 | remainingAccounts: [ 48 | { 49 | isSigner: false, 50 | address: tokenAccount, 51 | isWritable: false, 52 | }, 53 | ], 54 | }; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CandyMachineBuildersClient'; 2 | export * from './CandyMachineClient'; 3 | export * from './CandyMachineGuardsClient'; 4 | export * from './CandyMachinePdasClient'; 5 | export * from './errors'; 6 | export * from './guards'; 7 | export * from './models'; 8 | export * from './operations'; 9 | export * from './plugin'; 10 | export * from './programs'; 11 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/models/CandyMachineItem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represent an item inside a Candy Machine that has been or 3 | * will eventually be minted into an NFT. 4 | * 5 | * It only contains the name and the URI of the NFT to be as 6 | * the rest of the data is shared by all NFTs and lives 7 | * in the Candy Machine configurations (e.g. `symbol`, `creators`, etc). 8 | * 9 | * @group Models 10 | */ 11 | export type CandyMachineItem = { 12 | /** The index of the config line. */ 13 | readonly index: number; 14 | 15 | /** Whether the item has been minted or not. */ 16 | readonly minted: boolean; 17 | 18 | /** The name of the NFT to be. */ 19 | readonly name: string; 20 | 21 | /** The URI of the NFT to be, pointing to some off-chain JSON Metadata. */ 22 | readonly uri: string; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CandyGuard'; 2 | export * from './CandyMachine'; 3 | export * from './CandyMachineItem'; 4 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callCandyGuardRoute'; 2 | export * from './createCandyGuard'; 3 | export * from './createCandyMachine'; 4 | export * from './deleteCandyGuard'; 5 | export * from './deleteCandyMachine'; 6 | export * from './findCandyGuardByAddress'; 7 | export * from './findCandyGuardsByAuthority'; 8 | export * from './findCandyMachineByAddress'; 9 | export * from './insertCandyMachineItems'; 10 | export * from './mintFromCandyMachine'; 11 | export * from './unwrapCandyGuard'; 12 | export * from './updateCandyGuard'; 13 | export * from './updateCandyMachine'; 14 | export * from './wrapCandyGuard'; 15 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineModule/programs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cusper as defaultCandyGuardCusper, 3 | PROGRAM_ID as DEFAULT_CANDY_GUARD_PROGRAM_ID, 4 | } from '@metaplex-foundation/mpl-candy-guard'; 5 | import { 6 | cusper as candyMachineCusper, 7 | PROGRAM_ID as CANDY_MACHINE_PROGRAM_ID, 8 | } from '@metaplex-foundation/mpl-candy-machine-core'; 9 | import { defaultCandyGuardNames } from './guards'; 10 | import { assert } from '@/utils'; 11 | import { ErrorWithLogs, Program, PublicKey } from '@/types'; 12 | 13 | /** @group Programs */ 14 | export const candyMachineProgram: Program = { 15 | name: 'CandyMachineProgram', 16 | address: CANDY_MACHINE_PROGRAM_ID, 17 | errorResolver: (error: ErrorWithLogs) => 18 | candyMachineCusper.errorFromProgramLogs(error.logs, false), 19 | }; 20 | 21 | /** @group Programs */ 22 | export type CandyGuardProgram = Program & { availableGuards: string[] }; 23 | 24 | export const isCandyGuardProgram = ( 25 | value: Program 26 | ): value is CandyGuardProgram => 27 | typeof value === 'object' && 'availableGuards' in value; 28 | 29 | export function assertCandyGuardProgram( 30 | value: Program 31 | ): asserts value is CandyGuardProgram { 32 | assert(isCandyGuardProgram(value), `Expected CandyGuardProgram model`); 33 | } 34 | 35 | /** @group Programs */ 36 | export const defaultCandyGuardProgram: CandyGuardProgram = { 37 | name: 'CandyGuardProgram', 38 | address: DEFAULT_CANDY_GUARD_PROGRAM_ID, 39 | errorResolver: (error: ErrorWithLogs) => 40 | defaultCandyGuardCusper.errorFromProgramLogs(error.logs, false), 41 | availableGuards: defaultCandyGuardNames, 42 | }; 43 | 44 | /** @group Programs */ 45 | export const gatewayProgram: Program = { 46 | name: 'GatewayProgram', 47 | address: new PublicKey('gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs'), 48 | }; 49 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/CandyMachinesV2BuildersClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createCandyMachineV2Builder, 3 | CreateCandyMachineV2BuilderParams, 4 | deleteCandyMachineV2Builder, 5 | DeleteCandyMachineV2BuilderParams, 6 | insertItemsToCandyMachineV2Builder, 7 | InsertItemsToCandyMachineV2BuilderParams, 8 | mintCandyMachineV2Builder, 9 | MintCandyMachineV2BuilderParams, 10 | updateCandyMachineV2Builder, 11 | UpdateCandyMachineV2BuilderParams, 12 | } from './operations'; 13 | import type { Metaplex } from '@/Metaplex'; 14 | import { TransactionBuilderOptions } from '@/utils'; 15 | 16 | /** 17 | * This client allows you to access the underlying Transaction Builders 18 | * for the write operations of the Candy Machine module. 19 | * 20 | * @see {@link CandyMachinesV2Client} 21 | * @group Module Builders 22 | */ 23 | export class CandyMachinesV2BuildersClient { 24 | constructor(protected readonly metaplex: Metaplex) {} 25 | 26 | /** {@inheritDoc createCandyMachineV2Builder} */ 27 | create( 28 | input: CreateCandyMachineV2BuilderParams, 29 | options?: TransactionBuilderOptions 30 | ) { 31 | return createCandyMachineV2Builder(this.metaplex, input, options); 32 | } 33 | 34 | /** {@inheritDoc deleteCandyMachineV2Builder} */ 35 | delete( 36 | input: DeleteCandyMachineV2BuilderParams, 37 | options?: TransactionBuilderOptions 38 | ) { 39 | return deleteCandyMachineV2Builder(this.metaplex, input, options); 40 | } 41 | 42 | /** {@inheritDoc insertItemsToCandyMachineV2Builder} */ 43 | insertItems( 44 | input: InsertItemsToCandyMachineV2BuilderParams, 45 | options?: TransactionBuilderOptions 46 | ) { 47 | return insertItemsToCandyMachineV2Builder(this.metaplex, input, options); 48 | } 49 | 50 | /** {@inheritDoc mintCandyMachineV2Builder} */ 51 | mint( 52 | input: MintCandyMachineV2BuilderParams, 53 | options?: TransactionBuilderOptions 54 | ) { 55 | return mintCandyMachineV2Builder(this.metaplex, input, options); 56 | } 57 | 58 | /** {@inheritDoc updateCandyMachineV2Builder} */ 59 | update( 60 | input: UpdateCandyMachineV2BuilderParams, 61 | options?: TransactionBuilderOptions 62 | ) { 63 | return updateCandyMachineV2Builder(this.metaplex, input, options); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/accounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CandyMachine, 3 | CollectionPDA, 4 | } from '@metaplex-foundation/mpl-candy-machine'; 5 | import { 6 | Account, 7 | getAccountParsingAndAssertingFunction, 8 | getAccountParsingFunction, 9 | MaybeAccount, 10 | } from '@/types'; 11 | 12 | /** @group Accounts */ 13 | export type CandyMachineV2Account = Account; 14 | 15 | /** @group Account Helpers */ 16 | export const parseCandyMachineV2Account = 17 | getAccountParsingFunction(CandyMachine); 18 | 19 | /** @group Account Helpers */ 20 | export const toCandyMachineV2Account = 21 | getAccountParsingAndAssertingFunction(CandyMachine); 22 | 23 | /** @group Accounts */ 24 | export type CandyMachineV2CollectionAccount = Account; 25 | 26 | /** @group Accounts */ 27 | export type MaybeCandyMachineV2CollectionAccount = MaybeAccount; 28 | 29 | /** @group Account Helpers */ 30 | export const parseCandyMachineV2CollectionAccount = 31 | getAccountParsingFunction(CollectionPDA); 32 | 33 | /** @group Account Helpers */ 34 | export const toCandyMachineV2CollectionAccount = 35 | getAccountParsingAndAssertingFunction(CollectionPDA); 36 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_NAME_LENGTH = 32; 2 | export const MAX_SYMBOL_LENGTH = 10; 3 | export const MAX_URI_LENGTH = 200; 4 | export const MAX_CREATOR_LIMIT = 5; 5 | export const MAX_CREATOR_LEN = 32 + 1 + 1; 6 | export const CONFIG_LINE_SIZE = 4 + MAX_NAME_LENGTH + 4 + MAX_URI_LENGTH; 7 | export const CONFIG_ARRAY_START = 8 | 8 + // key 9 | 32 + // authority 10 | 32 + // wallet 11 | 33 + // token mint 12 | 4 + 13 | 6 + // uuid 14 | 8 + // price 15 | 8 + // items available 16 | 9 + // go live 17 | 10 + // end settings 18 | 4 + 19 | MAX_SYMBOL_LENGTH + // u32 len + symbol 20 | 2 + // seller fee basis points 21 | 4 + 22 | MAX_CREATOR_LIMIT * MAX_CREATOR_LEN + // optional + u32 len + actual vec 23 | 8 + // max supply 24 | 1 + // is mutable 25 | 1 + // retain authority 26 | 1 + // option for hidden setting 27 | 4 + 28 | MAX_NAME_LENGTH + // name length, 29 | 4 + 30 | MAX_URI_LENGTH + // uri length, 31 | 32 + // hash 32 | 4 + // max number of lines; 33 | 8 + // items redeemed 34 | 1 + // whitelist option 35 | 1 + // whitelist mint mode 36 | 1 + // allow presale 37 | 9 + // discount price 38 | 32 + // mint key for whitelist 39 | 1 + 40 | 32 + 41 | 1; // gatekeeper 42 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/gpaBuilders.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { GpaBuilder } from '@/utils'; 3 | 4 | type AccountDiscriminator = [ 5 | number, 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number 13 | ]; 14 | // TODO(thlorenz): copied from candy machine SDK 15 | // SDK should either provide a GPA builder or expose this discriminator 16 | const candyMachineV2Discriminator: AccountDiscriminator = [ 17 | 51, 173, 177, 113, 25, 241, 109, 189, 18 | ]; 19 | 20 | const AUTHORITY = candyMachineV2Discriminator.length; 21 | const WALLET = AUTHORITY + PublicKey.default.toBytes().byteLength; 22 | 23 | export class CandyMachineV2GpaBuilder extends GpaBuilder { 24 | whereDiscriminator(discrimator: AccountDiscriminator) { 25 | return this.where(0, Buffer.from(discrimator)); 26 | } 27 | 28 | candyMachineAccounts() { 29 | return this.whereDiscriminator(candyMachineV2Discriminator); 30 | } 31 | 32 | // wallet same as solTreasury 33 | candyMachineAccountsForWallet(wallet: PublicKey) { 34 | return this.candyMachineAccounts().where(WALLET, wallet.toBase58()); 35 | } 36 | 37 | candyMachineAccountsForAuthority(authority: PublicKey) { 38 | return this.candyMachineAccounts().where(AUTHORITY, authority.toBase58()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import { 3 | CandyMachineData, 4 | configLineBeet, 5 | } from '@metaplex-foundation/mpl-candy-machine'; 6 | import { CONFIG_ARRAY_START, CONFIG_LINE_SIZE } from './constants'; 7 | import { CandyMachineV2Item } from './models'; 8 | import { removeEmptyChars } from '@/utils'; 9 | import { BigNumber, toBigNumber } from '@/types'; 10 | 11 | export function countCandyMachineV2Items(rawData: Buffer): BigNumber { 12 | const number = rawData.slice(CONFIG_ARRAY_START, CONFIG_ARRAY_START + 4); 13 | return toBigNumber(number, 'le'); 14 | } 15 | 16 | export function parseCandyMachineV2Items( 17 | rawData: Buffer 18 | ): CandyMachineV2Item[] { 19 | const configLinesStart = CONFIG_ARRAY_START + 4; 20 | const lines = []; 21 | const count = countCandyMachineV2Items(rawData).toNumber(); 22 | for (let i = 0; i < count; i++) { 23 | const [line] = configLineBeet.deserialize( 24 | rawData, 25 | configLinesStart + i * CONFIG_LINE_SIZE 26 | ); 27 | lines.push({ 28 | name: removeEmptyChars(line.name), 29 | uri: removeEmptyChars(line.uri), 30 | }); 31 | } 32 | return lines; 33 | } 34 | 35 | export function getCandyMachineV2AccountSizeFromData(data: CandyMachineData) { 36 | if (data.hiddenSettings != null) { 37 | return CONFIG_ARRAY_START; 38 | } 39 | const itemsAvailable = toBigNumber(data.itemsAvailable).toNumber(); 40 | return Math.ceil( 41 | CONFIG_ARRAY_START + 42 | 4 + 43 | itemsAvailable * CONFIG_LINE_SIZE + 44 | 8 + 45 | 2 * (itemsAvailable / 8 + 1) 46 | ); 47 | } 48 | 49 | export const getCandyMachineV2UuidFromAddress = ( 50 | candyMachineAddress: PublicKey 51 | ): string => { 52 | return candyMachineAddress.toBase58().slice(0, 6); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts'; 2 | export * from './CandyMachinesV2BuildersClient'; 3 | export * from './CandyMachinesV2Client'; 4 | export * from './errors'; 5 | export * from './gpaBuilders'; 6 | export * from './helpers'; 7 | export * from './models'; 8 | export * from './operations'; 9 | export * from './pdas'; 10 | export * from './plugin'; 11 | export * from './program'; 12 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CandyMachineV2'; 2 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createCandyMachineV2'; 2 | export * from './deleteCandyMachineV2'; 3 | export * from './findCandyMachineV2ByAddress'; 4 | export * from './findCandyMachinesV2ByPublicKeyField'; 5 | export * from './findMintedNftsByCandyMachineV2'; 6 | export * from './insertItemsToCandyMachineV2'; 7 | export * from './mintCandyMachineV2'; 8 | export * from './updateCandyMachineV2'; 9 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/pdas.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { CandyMachineV2Program } from './program'; 4 | import { Pda } from '@/types'; 5 | 6 | /** @group Pdas */ 7 | export const findCandyMachineV2CreatorPda = ( 8 | candyMachine: PublicKey, 9 | programId: PublicKey = CandyMachineV2Program.publicKey 10 | ): Pda => { 11 | return Pda.find(programId, [ 12 | Buffer.from('candy_machine', 'utf8'), 13 | candyMachine.toBuffer(), 14 | ]); 15 | }; 16 | 17 | /** @group Pdas */ 18 | export const findCandyMachineV2CollectionPda = ( 19 | candyMachine: PublicKey, 20 | programId: PublicKey = CandyMachineV2Program.publicKey 21 | ): Pda => { 22 | return Pda.find(programId, [ 23 | Buffer.from('collection', 'utf8'), 24 | candyMachine.toBuffer(), 25 | ]); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/js/src/plugins/candyMachineV2Module/program.ts: -------------------------------------------------------------------------------- 1 | import { PROGRAM_ID } from '@metaplex-foundation/mpl-candy-machine'; 2 | import { CandyMachineV2GpaBuilder } from './gpaBuilders'; 3 | import { Metaplex } from '@/Metaplex'; 4 | 5 | /** @group Programs */ 6 | export const CandyMachineV2Program = { 7 | publicKey: PROGRAM_ID, 8 | 9 | accounts(metaplex: Metaplex) { 10 | return new CandyMachineV2GpaBuilder(metaplex, this.publicKey); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/js/src/plugins/corePlugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | -------------------------------------------------------------------------------- /packages/js/src/plugins/corePlugins/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex } from '../../Metaplex'; 2 | 3 | // Low-level modules. 4 | import { identityModule } from '../identityModule'; 5 | import { storageModule } from '../storageModule'; 6 | import { rpcModule } from '../rpcModule'; 7 | import { operationModule } from '../operationModule'; 8 | import { programModule } from '../programModule'; 9 | import { utilsModule } from '../utilsModule'; 10 | 11 | // Default drivers. 12 | import { guestIdentity } from '../guestIdentity'; 13 | import { irysStorage } from '../irysStorage'; 14 | 15 | // Verticals. 16 | import { systemModule } from '../systemModule'; 17 | import { tokenModule } from '../tokenModule'; 18 | import { nftModule } from '../nftModule'; 19 | import { candyMachineV2Module } from '../candyMachineV2Module'; 20 | import { candyMachineModule } from '../candyMachineModule'; 21 | import { auctionHouseModule } from '../auctionHouseModule'; 22 | 23 | export const corePlugins = () => ({ 24 | install(metaplex: Metaplex) { 25 | // Low-level modules. 26 | metaplex.use(identityModule()); 27 | metaplex.use(storageModule()); 28 | metaplex.use(rpcModule()); 29 | metaplex.use(operationModule()); 30 | metaplex.use(programModule()); 31 | metaplex.use(utilsModule()); 32 | 33 | // Default drivers. 34 | metaplex.use(guestIdentity()); 35 | metaplex.use(irysStorage()); 36 | 37 | // Verticals. 38 | metaplex.use(systemModule()); 39 | metaplex.use(tokenModule()); 40 | metaplex.use(nftModule()); 41 | metaplex.use(candyMachineV2Module()); 42 | metaplex.use(candyMachineModule()); 43 | metaplex.use(auctionHouseModule()); 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/js/src/plugins/derivedIdentity/errors.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexError } from '@/errors'; 2 | 3 | /** @group Errors */ 4 | export class DerivedIdentityError extends MetaplexError { 5 | readonly name: string = 'DerivedIdentityError'; 6 | constructor(message: string, cause?: Error) { 7 | super(message, 'plugin', 'Derived Identity', cause); 8 | } 9 | } 10 | 11 | /** @group Errors */ 12 | export class UninitializedDerivedIdentityError extends DerivedIdentityError { 13 | constructor() { 14 | const message = 15 | 'The derived identity module has not been initialized. ' + 16 | 'Before using the derived identity, you must provide a message that ' + 17 | 'will be used to derived a Keypair from the current identity. ' + 18 | 'You may do that by calling "metaplex.derivedIdentity().deriveFrom(message)".'; 19 | super(message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/js/src/plugins/derivedIdentity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DerivedIdentityClient'; 2 | export * from './errors'; 3 | export * from './plugin'; 4 | -------------------------------------------------------------------------------- /packages/js/src/plugins/derivedIdentity/plugin.ts: -------------------------------------------------------------------------------- 1 | import { DerivedIdentityClient } from './DerivedIdentityClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const derivedIdentity = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const derivedIdentityClient = new DerivedIdentityClient(metaplex); 9 | metaplex.derivedIdentity = () => derivedIdentityClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | derivedIdentity(): DerivedIdentityClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/guestIdentity/GuestIdentityDriver.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Transaction } from '@solana/web3.js'; 2 | import { IdentityDriver } from '../identityModule'; 3 | import { OperationUnauthorizedForGuestsError } from '@/errors'; 4 | 5 | export class GuestIdentityDriver implements IdentityDriver { 6 | public readonly publicKey: PublicKey; 7 | 8 | constructor(publicKey?: PublicKey) { 9 | this.publicKey = publicKey ?? PublicKey.default; 10 | } 11 | 12 | public async signMessage(): Promise { 13 | throw new OperationUnauthorizedForGuestsError('signMessage'); 14 | } 15 | 16 | public async signTransaction(): Promise { 17 | throw new OperationUnauthorizedForGuestsError('signTransaction'); 18 | } 19 | 20 | public async signAllTransactions(): Promise { 21 | throw new OperationUnauthorizedForGuestsError('signAllTransactions'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/js/src/plugins/guestIdentity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GuestIdentityDriver'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/guestIdentity/plugin.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { GuestIdentityDriver } from './GuestIdentityDriver'; 3 | import { Metaplex } from '@/Metaplex'; 4 | import { MetaplexPlugin } from '@/types'; 5 | 6 | /** @group Plugins */ 7 | export const guestIdentity = (publicKey?: PublicKey): MetaplexPlugin => ({ 8 | install(metaplex: Metaplex) { 9 | metaplex.identity().setDriver(new GuestIdentityDriver(publicKey)); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/js/src/plugins/identityModule/IdentityClient.ts: -------------------------------------------------------------------------------- 1 | import * as ed25519 from '@noble/ed25519'; 2 | import { PublicKey, Transaction } from '@solana/web3.js'; 3 | import { IdentityDriver } from './IdentityDriver'; 4 | import { 5 | HasDriver, 6 | IdentitySigner, 7 | isSigner, 8 | KeypairSigner, 9 | Signer, 10 | } from '@/types'; 11 | import { DriverNotProvidedError } from '@/errors'; 12 | 13 | /** 14 | * @group Modules 15 | */ 16 | export class IdentityClient 17 | implements HasDriver, IdentitySigner 18 | { 19 | private _driver: IdentityDriver | null = null; 20 | 21 | driver(): IdentityDriver { 22 | if (!this._driver) { 23 | throw new DriverNotProvidedError('IdentityDriver'); 24 | } 25 | 26 | return this._driver; 27 | } 28 | 29 | setDriver(newDriver: IdentityDriver): void { 30 | this._driver = newDriver; 31 | } 32 | 33 | get publicKey(): PublicKey { 34 | return this.driver().publicKey; 35 | } 36 | 37 | get secretKey(): Uint8Array | undefined { 38 | return this.driver().secretKey; 39 | } 40 | 41 | signMessage(message: Uint8Array): Promise { 42 | return this.driver().signMessage(message); 43 | } 44 | 45 | signTransaction(transaction: Transaction): Promise { 46 | return this.driver().signTransaction(transaction); 47 | } 48 | 49 | signAllTransactions(transactions: Transaction[]): Promise { 50 | return this.driver().signAllTransactions(transactions); 51 | } 52 | 53 | verifyMessage(message: Uint8Array, signature: Uint8Array): boolean { 54 | return ed25519.sync.verify(message, signature, this.publicKey.toBytes()); 55 | } 56 | 57 | equals(that: Signer | PublicKey): boolean { 58 | if (isSigner(that)) { 59 | that = that.publicKey; 60 | } 61 | 62 | return this.publicKey.equals(that); 63 | } 64 | 65 | hasSecretKey(): this is KeypairSigner { 66 | return this.secretKey != null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/js/src/plugins/identityModule/IdentityDriver.ts: -------------------------------------------------------------------------------- 1 | import { IdentitySigner } from '@/types'; 2 | 3 | export type IdentityDriver = IdentitySigner & { 4 | secretKey?: Uint8Array; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/js/src/plugins/identityModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | export * from './IdentityClient'; 3 | export * from './IdentityDriver'; 4 | -------------------------------------------------------------------------------- /packages/js/src/plugins/identityModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { IdentityClient } from './IdentityClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const identityModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const identityClient = new IdentityClient(); 9 | metaplex.identity = () => identityClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | identity(): IdentityClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auctionHouseModule'; 2 | export * from './irysStorage'; 3 | export * from './candyMachineModule'; 4 | export * from './candyMachineV2Module'; 5 | export * from './corePlugins'; 6 | export * from './derivedIdentity'; 7 | export * from './guestIdentity'; 8 | export * from './identityModule'; 9 | export * from './keypairIdentity'; 10 | export * from './mockStorage'; 11 | export * from './nftModule'; 12 | export * from './operationModule'; 13 | export * from './programModule'; 14 | export * from './rpcModule'; 15 | export * from './storageModule'; 16 | export * from './systemModule'; 17 | export * from './tokenModule'; 18 | export * from './utilsModule'; 19 | export * from './walletAdapterIdentity'; 20 | -------------------------------------------------------------------------------- /packages/js/src/plugins/irysStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IrysStorageDriver'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/irysStorage/plugin.ts: -------------------------------------------------------------------------------- 1 | import { IrysOptions, IrysStorageDriver } from './IrysStorageDriver'; 2 | import { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | export const irysStorage = (options: IrysOptions = {}): MetaplexPlugin => ({ 6 | install(metaplex: Metaplex) { 7 | metaplex.storage().setDriver(new IrysStorageDriver(metaplex, options)); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/js/src/plugins/keypairIdentity/KeypairIdentityDriver.ts: -------------------------------------------------------------------------------- 1 | import * as ed25519 from '@noble/ed25519'; 2 | import { Keypair, PublicKey, Transaction } from '@solana/web3.js'; 3 | import { IdentityDriver } from '../identityModule'; 4 | import { KeypairSigner } from '@/types'; 5 | 6 | export class KeypairIdentityDriver implements IdentityDriver, KeypairSigner { 7 | public readonly keypair: Keypair; 8 | public readonly publicKey: PublicKey; 9 | public readonly secretKey: Uint8Array; 10 | 11 | constructor(keypair: Keypair) { 12 | this.keypair = keypair; 13 | this.publicKey = keypair.publicKey; 14 | this.secretKey = keypair.secretKey; 15 | } 16 | 17 | public async signMessage(message: Uint8Array): Promise { 18 | return ed25519.sync.sign(message, this.secretKey.slice(0, 32)); 19 | } 20 | 21 | public async signTransaction(transaction: Transaction): Promise { 22 | transaction.partialSign(this.keypair); 23 | 24 | return transaction; 25 | } 26 | 27 | public async signAllTransactions( 28 | transactions: Transaction[] 29 | ): Promise { 30 | return Promise.all( 31 | transactions.map((transaction) => this.signTransaction(transaction)) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/js/src/plugins/keypairIdentity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KeypairIdentityDriver'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/keypairIdentity/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import { KeypairIdentityDriver } from './KeypairIdentityDriver'; 3 | import { Metaplex } from '@/Metaplex'; 4 | import { MetaplexPlugin } from '@/types'; 5 | 6 | export const keypairIdentity = (keypair: Keypair): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | metaplex.identity().setDriver(new KeypairIdentityDriver(keypair)); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/js/src/plugins/mockStorage/MockStorageDriver.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexFile, StorageDriver } from '../storageModule'; 2 | import { Amount, BigNumber, lamports, toBigNumber } from '@/types'; 3 | import { AssetNotFoundError } from '@/errors'; 4 | 5 | const DEFAULT_BASE_URL = 'https://mockstorage.example.com/'; 6 | const DEFAULT_COST_PER_BYTE = 1; 7 | 8 | export type MockStorageOptions = { 9 | baseUrl?: string; 10 | costPerByte?: BigNumber | number; 11 | }; 12 | 13 | export class MockStorageDriver implements StorageDriver { 14 | protected cache: Record = {}; 15 | public readonly baseUrl: string; 16 | public readonly costPerByte: BigNumber; 17 | 18 | constructor(options?: MockStorageOptions) { 19 | this.baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL; 20 | this.costPerByte = toBigNumber( 21 | options?.costPerByte != null 22 | ? options?.costPerByte 23 | : DEFAULT_COST_PER_BYTE 24 | ); 25 | } 26 | 27 | async getUploadPrice(bytes: number): Promise { 28 | return lamports(this.costPerByte.muln(bytes)); 29 | } 30 | 31 | async upload(file: MetaplexFile): Promise { 32 | const uri = `${this.baseUrl}${file.uniqueName}`; 33 | this.cache[uri] = file; 34 | 35 | return uri; 36 | } 37 | 38 | async download(uri: string): Promise { 39 | const file = this.cache[uri]; 40 | 41 | if (!file) { 42 | throw new AssetNotFoundError(uri); 43 | } 44 | 45 | return file; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/js/src/plugins/mockStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | -------------------------------------------------------------------------------- /packages/js/src/plugins/mockStorage/plugin.ts: -------------------------------------------------------------------------------- 1 | import { MockStorageDriver, MockStorageOptions } from './MockStorageDriver'; 2 | import { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | export const mockStorage = (options?: MockStorageOptions): MetaplexPlugin => ({ 6 | install(metaplex: Metaplex) { 7 | metaplex.storage().setDriver(new MockStorageDriver(options)); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/errors.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { MetadataDelegateType, TokenDelegateType } from './DelegateType'; 3 | import { MetaplexError } from '@/errors'; 4 | 5 | /** @group Errors */ 6 | export class NftError extends MetaplexError { 7 | readonly name: string = 'NftError'; 8 | constructor(message: string, cause?: Error) { 9 | super(message, 'plugin', 'NFT', cause); 10 | } 11 | } 12 | 13 | /** @group Errors */ 14 | export class ParentCollectionMissingError extends NftError { 15 | readonly name: string = 'ParentCollectionMissingError'; 16 | constructor(mint: PublicKey, operation: string) { 17 | const message = 18 | `You are trying to send the operation [${operation}] which requires the NFT to have ` + 19 | `a parent collection but that is not the case for the NFT at address [${mint}]. ` + 20 | 'Ensure the NFT you are interacting with has a parent collection.'; 21 | super(message); 22 | } 23 | } 24 | 25 | /** @group Errors */ 26 | export class DelegateRoleRequiredDataError extends NftError { 27 | readonly name: string = 'DelegateRoleRequiredDataError'; 28 | constructor(type: MetadataDelegateType | TokenDelegateType) { 29 | const message = 30 | `You are trying to approve a delegate of type "${type}" ` + 31 | `but did not provide any data for that role. Please provide the "data" ` + 32 | 'attribute as the SDK cannot provide a default value for that role.'; 33 | super(message); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import type { Nft, NftWithToken, Sft, SftWithToken } from './models'; 3 | import type { Metadata } from './models/Metadata'; 4 | import { PublicKeyValues, toPublicKey } from '@/types'; 5 | 6 | export type HasMintAddress = 7 | | Nft 8 | | Sft 9 | | NftWithToken 10 | | SftWithToken 11 | | Metadata 12 | | PublicKey; 13 | 14 | export const toMintAddress = ( 15 | value: PublicKeyValues | HasMintAddress 16 | ): PublicKey => { 17 | return typeof value === 'object' && 'mintAddress' in value 18 | ? value.mintAddress 19 | : toPublicKey(value); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts'; 2 | export * from './Authorization'; 3 | export * from './DelegateInput'; 4 | export * from './DelegateType'; 5 | export * from './errors'; 6 | export * from './gpaBuilders'; 7 | export * from './helpers'; 8 | export * from './models'; 9 | export * from './NftBuildersClient'; 10 | export * from './NftClient'; 11 | export * from './NftPdasClient'; 12 | export * from './operations'; 13 | export * from './pdas'; 14 | export * from './plugin'; 15 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/models/JsonMetadata.ts: -------------------------------------------------------------------------------- 1 | /** @group Models */ 2 | export type JsonMetadata = { 3 | name?: string; 4 | symbol?: string; 5 | description?: string; 6 | seller_fee_basis_points?: number; 7 | image?: Uri; 8 | animation_url?: Uri; 9 | external_url?: Uri; 10 | attributes?: Array<{ 11 | trait_type?: string; 12 | value?: string; 13 | [key: string]: unknown; 14 | }>; 15 | properties?: { 16 | creators?: Array<{ 17 | address?: string; 18 | share?: number; 19 | [key: string]: unknown; 20 | }>; 21 | files?: Array<{ 22 | type?: string; 23 | uri?: Uri; 24 | [key: string]: unknown; 25 | }>; 26 | [key: string]: unknown; 27 | }; 28 | collection?: { 29 | name?: string; 30 | family?: string; 31 | [key: string]: unknown; 32 | }; 33 | [key: string]: unknown; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JsonMetadata'; 2 | export * from './Metadata'; 3 | export * from './Nft'; 4 | export * from './NftEdition'; 5 | export * from './Sft'; 6 | -------------------------------------------------------------------------------- /packages/js/src/plugins/nftModule/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './approveNftCollectionAuthority'; 2 | export * from './approveNftDelegate'; 3 | export * from './approveNftUseAuthority'; 4 | export * from './createNft'; 5 | export * from './createSft'; 6 | export * from './deleteNft'; 7 | export * from './createCompressedNft'; 8 | export * from './transferCompressedNft'; 9 | export * from './findNftByAssetId'; 10 | export * from './findNftByMetadata'; 11 | export * from './findNftByMint'; 12 | export * from './findNftByToken'; 13 | export * from './findNftsByCreator'; 14 | export * from './findNftsByMintList'; 15 | export * from './findNftsByOwner'; 16 | export * from './findNftsByUpdateAuthority'; 17 | export * from './freezeDelegatedNft'; 18 | export * from './loadMetadata'; 19 | export * from './lockNft'; 20 | export * from './migrateToSizedCollectionNft'; 21 | export * from './mintNft'; 22 | export * from './printNewEdition'; 23 | export * from './revokeNftCollectionAuthority'; 24 | export * from './revokeNftDelegate'; 25 | export * from './revokeNftUseAuthority'; 26 | export * from './thawDelegatedNft'; 27 | export * from './transferNft'; 28 | export * from './unlockNft'; 29 | export * from './unverifyNftCollection'; 30 | export * from './unverifyNftCreator'; 31 | export * from './updateNft'; 32 | export * from './uploadMetadata'; 33 | export * from './useNft'; 34 | export * from './verifyNftCollection'; 35 | export * from './verifyNftCreator'; 36 | -------------------------------------------------------------------------------- /packages/js/src/plugins/operationModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OperationClient'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/operationModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { OperationClient } from './OperationClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const operationModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const operationClient = new OperationClient(metaplex); 9 | metaplex.operations = () => operationClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | operations(): OperationClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/programModule/ProgramClient.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { ProgramNotRecognizedError } from '@/errors'; 4 | import { Program, Cluster } from '@/types'; 5 | 6 | /** 7 | * @group Modules 8 | */ 9 | export class ProgramClient { 10 | protected programs: Program[] = []; 11 | constructor(protected readonly metaplex: Metaplex) {} 12 | 13 | register(program: Program): void { 14 | this.programs.unshift(program); 15 | } 16 | 17 | all(overrides: Program[] = []): Program[] { 18 | return [...overrides, ...this.programs]; 19 | } 20 | 21 | allForCluster(cluster: Cluster, overrides: Program[] = []): Program[] { 22 | return this.all(overrides).filter((program) => { 23 | return program.clusterFilter?.(cluster) ?? true; 24 | }); 25 | } 26 | 27 | allForCurrentCluster(overrides: Program[] = []): Program[] { 28 | return this.allForCluster(this.metaplex.cluster, overrides); 29 | } 30 | 31 | get( 32 | nameOrAddress: string | PublicKey, 33 | overrides: Program[] = [] 34 | ): T { 35 | const programs = this.allForCurrentCluster(overrides); 36 | const program = 37 | typeof nameOrAddress === 'string' 38 | ? programs.find((program) => program.name === nameOrAddress) 39 | : programs.find((program) => program.address.equals(nameOrAddress)); 40 | 41 | if (!program) { 42 | throw new ProgramNotRecognizedError(nameOrAddress, this.metaplex.cluster); 43 | } 44 | 45 | return program as T; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/js/src/plugins/programModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProgramClient'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/programModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { ProgramClient } from './ProgramClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const programModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const programClient = new ProgramClient(metaplex); 9 | metaplex.programs = () => programClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | programs(): ProgramClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/rpcModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | export * from './RpcClient'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/rpcModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { RpcClient } from './RpcClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const rpcModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const rpcClient = new RpcClient(metaplex); 9 | metaplex.rpc = () => rpcClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | rpc(): RpcClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/storageModule/StorageDriver.ts: -------------------------------------------------------------------------------- 1 | import { RequestInit } from 'node-fetch'; 2 | import { MetaplexFile } from './MetaplexFile'; 3 | import { Amount } from '@/types'; 4 | 5 | export type StorageDriver = { 6 | getUploadPrice: (bytes: number) => Promise; 7 | upload: (file: MetaplexFile) => Promise; 8 | uploadAll?: (files: MetaplexFile[]) => Promise; 9 | download?: ( 10 | uri: string, 11 | options?: StorageDownloadOptions 12 | ) => Promise; 13 | getUploadPriceForFiles?: (files: MetaplexFile[]) => Promise; 14 | }; 15 | 16 | export type StorageDownloadOptions = Omit & { 17 | signal?: AbortSignal | null; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/js/src/plugins/storageModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MetaplexFile'; 2 | export * from './plugin'; 3 | export * from './StorageClient'; 4 | export * from './StorageDriver'; 5 | -------------------------------------------------------------------------------- /packages/js/src/plugins/storageModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { StorageClient } from './StorageClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const storageModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const storageClient = new StorageClient(); 9 | metaplex.storage = () => storageClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | storage(): StorageClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/systemModule/SystemBuildersClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAccountBuilder, 3 | CreateAccountBuilderParams, 4 | transferSolBuilder, 5 | TransferSolBuilderParams, 6 | } from './operations'; 7 | import type { Metaplex } from '@/Metaplex'; 8 | import { TransactionBuilderOptions } from '@/utils'; 9 | 10 | /** 11 | * This client allows you to access the underlying Transaction Builders 12 | * for the write operations of the System module. 13 | * 14 | * @see {@link SystemClient} 15 | * @group Module Builders 16 | * */ 17 | export class SystemBuildersClient { 18 | constructor(protected readonly metaplex: Metaplex) {} 19 | 20 | /** {@inheritDoc createAccountBuilder} */ 21 | createAccount( 22 | input: CreateAccountBuilderParams, 23 | options?: TransactionBuilderOptions 24 | ) { 25 | return createAccountBuilder(this.metaplex, input, options); 26 | } 27 | 28 | /** {@inheritDoc transferSolBuilder} */ 29 | transferSol( 30 | input: TransferSolBuilderParams, 31 | options?: TransactionBuilderOptions 32 | ) { 33 | return transferSolBuilder(this.metaplex, input, options); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/js/src/plugins/systemModule/SystemClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateAccountInput, 3 | createAccountOperation, 4 | TransferSolInput, 5 | transferSolOperation, 6 | } from './operations'; 7 | import { SystemBuildersClient } from './SystemBuildersClient'; 8 | import type { Metaplex } from '@/Metaplex'; 9 | import { OperationOptions } from '@/types'; 10 | 11 | /** 12 | * This is a client for the System module. 13 | * 14 | * It enables us to interact with the System program in order to 15 | * create uninitialized accounts and transfer SOL. 16 | * 17 | * You may access this client via the `system()` method of your `Metaplex` instance. 18 | * 19 | * ```ts 20 | * const systemClient = metaplex.system(); 21 | * ``` 22 | * 23 | * @example 24 | * You can create a new uninitialized account with a given space in bytes 25 | * using the code below. 26 | * 27 | * ```ts 28 | * const { newAccount } = await metaplex.system().createAccount({ space: 42 }); 29 | * ``` 30 | * 31 | * @group Modules 32 | */ 33 | export class SystemClient { 34 | constructor(protected readonly metaplex: Metaplex) {} 35 | 36 | /** 37 | * You may use the `builders()` client to access the 38 | * underlying Transaction Builders of this module. 39 | * 40 | * ```ts 41 | * const buildersClient = metaplex.system().builders(); 42 | * ``` 43 | */ 44 | builders() { 45 | return new SystemBuildersClient(this.metaplex); 46 | } 47 | 48 | /** {@inheritDoc createAccountOperation} */ 49 | createAccount(input: CreateAccountInput, options?: OperationOptions) { 50 | return this.metaplex 51 | .operations() 52 | .execute(createAccountOperation(input), options); 53 | } 54 | 55 | /** {@inheritDoc transferSolOperation} */ 56 | transferSol(input: TransferSolInput, options?: OperationOptions) { 57 | return this.metaplex 58 | .operations() 59 | .execute(transferSolOperation(input), options); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/js/src/plugins/systemModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './operations'; 2 | export * from './plugin'; 3 | export * from './SystemBuildersClient'; 4 | export * from './SystemClient'; 5 | -------------------------------------------------------------------------------- /packages/js/src/plugins/systemModule/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createAccount'; 2 | export * from './transferSol'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/systemModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { SystemProgram } from '@solana/web3.js'; 2 | import { ProgramClient } from '../programModule'; 3 | import { 4 | createAccountOperation, 5 | createAccountOperationHandler, 6 | transferSolOperation, 7 | transferSolOperationHandler, 8 | } from './operations'; 9 | import { SystemClient } from './SystemClient'; 10 | import type { MetaplexPlugin, Program } from '@/types'; 11 | import type { Metaplex } from '@/Metaplex'; 12 | 13 | /** 14 | * @group Plugins 15 | */ 16 | /** @group Plugins */ 17 | export const systemModule = (): MetaplexPlugin => ({ 18 | install(metaplex: Metaplex) { 19 | // Program. 20 | const systemProgram = { 21 | name: 'SystemProgram', 22 | address: SystemProgram.programId, 23 | }; 24 | metaplex.programs().register(systemProgram); 25 | metaplex.programs().getSystem = function ( 26 | this: ProgramClient, 27 | programs?: Program[] 28 | ) { 29 | return this.get(systemProgram.name, programs); 30 | }; 31 | 32 | // Operations. 33 | const op = metaplex.operations(); 34 | op.register(createAccountOperation, createAccountOperationHandler); 35 | op.register(transferSolOperation, transferSolOperationHandler); 36 | 37 | metaplex.system = function () { 38 | return new SystemClient(this); 39 | }; 40 | }, 41 | }); 42 | 43 | declare module '../../Metaplex' { 44 | interface Metaplex { 45 | system(): SystemClient; 46 | } 47 | } 48 | 49 | declare module '../programModule/ProgramClient' { 50 | interface ProgramClient { 51 | getSystem(programs?: Program[]): Program; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/TokenPdasClient.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex } from '@/Metaplex'; 2 | import { Pda, Program, PublicKey } from '@/types'; 3 | 4 | /** 5 | * This client allows you to build PDAs related to the Token module. 6 | * 7 | * @see {@link TokenClient} 8 | * @group Module Pdas 9 | */ 10 | export class TokenPdasClient { 11 | constructor(protected readonly metaplex: Metaplex) {} 12 | 13 | /** Finds the address of the Associated Token Account. */ 14 | associatedTokenAccount({ 15 | mint, 16 | owner, 17 | programs, 18 | }: { 19 | /** The address of the mint account. */ 20 | mint: PublicKey; 21 | /** The address of the owner account. */ 22 | owner: PublicKey; 23 | /** An optional set of programs that override the registered ones. */ 24 | programs?: Program[]; 25 | }): Pda { 26 | const tokenProgram = this.metaplex.programs().getToken(programs); 27 | const associatedTokenProgram = this.metaplex 28 | .programs() 29 | .getAssociatedToken(programs); 30 | return Pda.find(associatedTokenProgram.address, [ 31 | owner.toBuffer(), 32 | tokenProgram.address.toBuffer(), 33 | mint.toBuffer(), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/accounts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountLayout as SplTokenAccountLayout, 3 | MintLayout as SplMintAccountLayout, 4 | RawAccount as SplTokenAccount, 5 | RawMint as SplMintAccount, 6 | } from '@solana/spl-token'; 7 | import { NotYetImplementedError } from '@/errors'; 8 | import { 9 | Account, 10 | SolitaType, 11 | getAccountParsingAndAssertingFunction, 12 | getAccountParsingFunction, 13 | } from '@/types'; 14 | 15 | const mintAccountParser: SolitaType = { 16 | name: 'MintAccount', 17 | deserialize: (data: Buffer, offset?: number) => { 18 | const span = SplMintAccountLayout.getSpan(data, offset); 19 | const decoded = SplMintAccountLayout.decode(data, offset); 20 | return [decoded, span]; 21 | }, 22 | fromArgs() { 23 | throw new NotYetImplementedError(); 24 | }, 25 | }; 26 | 27 | /** @group Accounts */ 28 | export type MintAccount = Account; 29 | 30 | /** @group Account Helpers */ 31 | export const parseMintAccount = getAccountParsingFunction(mintAccountParser); 32 | 33 | /** @group Account Helpers */ 34 | export const toMintAccount = 35 | getAccountParsingAndAssertingFunction(mintAccountParser); 36 | 37 | const tokenAccountParser: SolitaType = { 38 | name: 'TokenAccount', 39 | deserialize: (data: Buffer, offset?: number) => { 40 | const span = SplTokenAccountLayout.getSpan(data, offset); 41 | const decoded = SplTokenAccountLayout.decode(data, offset); 42 | return [decoded, span]; 43 | }, 44 | fromArgs() { 45 | throw new NotYetImplementedError(); 46 | }, 47 | }; 48 | 49 | /** @group Accounts */ 50 | export type TokenAccount = Account; 51 | 52 | /** @group Account Helpers */ 53 | export const parseTokenAccount = getAccountParsingFunction(tokenAccountParser); 54 | 55 | /** @group Account Helpers */ 56 | export const toTokenAccount = 57 | getAccountParsingAndAssertingFunction(tokenAccountParser); 58 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | /** @group Constants */ 4 | export const WRAPPED_SOL_MINT = new PublicKey( 5 | 'So11111111111111111111111111111111111111112' 6 | ); 7 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/errors.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { MetaplexError } from '@/errors'; 3 | 4 | /** @group Errors */ 5 | export class TokenError extends MetaplexError { 6 | readonly name: string = 'TokenError'; 7 | constructor(message: string, cause?: Error) { 8 | super(message, 'plugin', 'Token', cause); 9 | } 10 | } 11 | 12 | /** @group Errors */ 13 | export class MintAuthorityMustBeSignerToMintInitialSupplyError extends TokenError { 14 | readonly name: string = 'MintAuthorityMustBeSignerToMintInitialSupplyError'; 15 | constructor() { 16 | const message = 17 | 'You are trying to create a Mint and a Token account and to send an initial ' + 18 | 'supply of token to the newly created Token account. The issue is, you have provided ' + 19 | "a Mint Authority as a Public Key which means we don't have the rights to send this transaction. " + 20 | 'Please provide the Mint Authority as a Signer when using the "createTokenWithMint" operation ' + 21 | ', so we can send the initial supply. Alternative, remove the initial supply from the operation for it to succeed.'; 22 | super(message); 23 | } 24 | } 25 | 26 | /** @group Errors */ 27 | export class TokenAndMintDoNotMatchError extends TokenError { 28 | constructor(token: PublicKey, tokenMint: PublicKey, mint: PublicKey) { 29 | const message = 30 | `The provided Token and Mint accounts do not match. That is, the mint address [${tokenMint}] ` + 31 | `stored in the Token account [${token}] do not match the address of the Mint account [${mint}]. ` + 32 | 'Please provide a Token account that belongs to the provided Mint account.'; 33 | super(message); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/gpaBuilders.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { MINT_SIZE, ACCOUNT_SIZE, TOKEN_PROGRAM_ID } from '@solana/spl-token'; 3 | import { Metaplex } from '@/Metaplex'; 4 | import { GpaBuilder } from '@/utils'; 5 | import { BigNumber } from '@/types'; 6 | 7 | export class MintGpaBuilder extends GpaBuilder { 8 | constructor(metaplex: Metaplex, programId?: PublicKey) { 9 | super(metaplex, programId ?? TOKEN_PROGRAM_ID); 10 | this.whereSize(MINT_SIZE); 11 | } 12 | 13 | whereDoesntHaveMintAuthority() { 14 | return this.where(0, 0); 15 | } 16 | 17 | whereHasMintAuthority() { 18 | return this.where(0, 1); 19 | } 20 | 21 | whereMintAuthority(mintAuthority: PublicKey) { 22 | return this.whereHasMintAuthority().where(4, mintAuthority); 23 | } 24 | 25 | whereSupply(supply: number | BigNumber) { 26 | return this.where(36, supply); 27 | } 28 | 29 | // TODO(loris): Map the rest of the layout. 30 | // https://github.com/solana-labs/solana-program-library/blob/master/token/js/src/state/mint.ts#L43 31 | } 32 | 33 | export class TokenGpaBuilder extends GpaBuilder { 34 | constructor(metaplex: Metaplex, programId?: PublicKey) { 35 | super(metaplex, programId ?? TOKEN_PROGRAM_ID); 36 | this.whereSize(ACCOUNT_SIZE); 37 | } 38 | 39 | selectMint() { 40 | return this.slice(0, 32); 41 | } 42 | 43 | whereMint(mint: PublicKey) { 44 | return this.where(0, mint); 45 | } 46 | 47 | selectOwner() { 48 | return this.slice(32, 32); 49 | } 50 | 51 | whereOwner(owner: PublicKey) { 52 | return this.where(32, owner); 53 | } 54 | 55 | selectAmount() { 56 | return this.slice(64, 8); 57 | } 58 | 59 | whereAmount(amount: number | BigNumber) { 60 | return this.where(64, amount); 61 | } 62 | 63 | whereDoesntHaveDelegate() { 64 | return this.where(72, 0); 65 | } 66 | 67 | whereHasDelegate() { 68 | return this.where(72, 1); 69 | } 70 | 71 | whereDelegate(delegate: PublicKey) { 72 | return this.whereHasDelegate().where(76, delegate); 73 | } 74 | 75 | // TODO(loris): Map the rest of the layout. 76 | // https://github.com/solana-labs/solana-program-library/blob/master/token/js/src/state/account.ts#L59 77 | } 78 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts'; 2 | export * from './constants'; 3 | export * from './errors'; 4 | export * from './gpaBuilders'; 5 | export * from './models'; 6 | export * from './operations'; 7 | export * from './plugin'; 8 | export * from './program'; 9 | export * from './TokenBuildersClient'; 10 | export * from './TokenClient'; 11 | export * from './TokenPdasClient'; 12 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Mint'; 2 | export * from './Token'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/operations/findMintByAddress.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import { toMintAccount } from '../accounts'; 3 | import { Mint, toMint } from '../models/Mint'; 4 | import { 5 | Operation, 6 | OperationHandler, 7 | OperationScope, 8 | useOperation, 9 | } from '@/types'; 10 | import { Metaplex } from '@/Metaplex'; 11 | 12 | // ----------------- 13 | // Operation 14 | // ----------------- 15 | 16 | const Key = 'FindMintByAddressOperation' as const; 17 | 18 | /** 19 | * Finds a mint account by its address. 20 | * 21 | * ```ts 22 | * const mint = await metaplex.tokens().findMintByAddress({ address }); 23 | * ``` 24 | * 25 | * @group Operations 26 | * @category Constructors 27 | */ 28 | export const findMintByAddressOperation = 29 | useOperation(Key); 30 | 31 | /** 32 | * @group Operations 33 | * @category Types 34 | */ 35 | export type FindMintByAddressOperation = Operation< 36 | typeof Key, 37 | FindMintByAddressInput, 38 | Mint 39 | >; 40 | 41 | /** 42 | * @group Operations 43 | * @category Inputs 44 | */ 45 | export type FindMintByAddressInput = { 46 | /** The address of the mint account. */ 47 | address: PublicKey; 48 | }; 49 | 50 | /** 51 | * @group Operations 52 | * @category Handlers 53 | */ 54 | export const findMintByAddressOperationHandler: OperationHandler = 55 | { 56 | handle: async ( 57 | operation: FindMintByAddressOperation, 58 | metaplex: Metaplex, 59 | scope: OperationScope 60 | ) => { 61 | const { commitment } = scope; 62 | const { address } = operation.input; 63 | 64 | const account = toMintAccount( 65 | await metaplex.rpc().getAccount(address, commitment) 66 | ); 67 | 68 | return toMint(account); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/operations/findTokenByAddress.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import { toTokenAccount } from '../accounts'; 3 | import { Token, toToken } from '../models/Token'; 4 | import { 5 | Operation, 6 | OperationHandler, 7 | OperationScope, 8 | useOperation, 9 | } from '@/types'; 10 | import { Metaplex } from '@/Metaplex'; 11 | 12 | // ----------------- 13 | // Operation 14 | // ----------------- 15 | 16 | const Key = 'FindTokenByAddressOperation' as const; 17 | 18 | /** 19 | * Finds a token account by its address. 20 | * 21 | * ```ts 22 | * const token = await metaplex.tokens().findTokenByAddress({ address }); 23 | * ``` 24 | * 25 | * @group Operations 26 | * @category Constructors 27 | */ 28 | export const findTokenByAddressOperation = 29 | useOperation(Key); 30 | 31 | /** 32 | * @group Operations 33 | * @category Types 34 | */ 35 | export type FindTokenByAddressOperation = Operation< 36 | typeof Key, 37 | FindTokenByAddressInput, 38 | Token 39 | >; 40 | 41 | /** 42 | * @group Operations 43 | * @category Inputs 44 | */ 45 | export type FindTokenByAddressInput = { 46 | /** The address of the token account. */ 47 | address: PublicKey; 48 | }; 49 | 50 | /** 51 | * @group Operations 52 | * @category Handlers 53 | */ 54 | export const findTokenByAddressOperationHandler: OperationHandler = 55 | { 56 | handle: async ( 57 | operation: FindTokenByAddressOperation, 58 | metaplex: Metaplex, 59 | scope: OperationScope 60 | ): Promise => { 61 | const { commitment } = scope; 62 | const { address } = operation.input; 63 | 64 | const account = toTokenAccount( 65 | await metaplex.rpc().getAccount(address, commitment) 66 | ); 67 | 68 | return toToken(account); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/operations/findTokenWithMintByAddress.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@solana/web3.js'; 2 | import { toMintAccount, toTokenAccount } from '../accounts'; 3 | import { toMint } from '../models/Mint'; 4 | import { TokenWithMint, toTokenWithMint } from '../models/Token'; 5 | import { 6 | Operation, 7 | OperationHandler, 8 | OperationScope, 9 | useOperation, 10 | } from '@/types'; 11 | import { Metaplex } from '@/Metaplex'; 12 | 13 | // ----------------- 14 | // Operation 15 | // ----------------- 16 | 17 | const Key = 'FindTokenWithMintByAddressOperation' as const; 18 | 19 | /** 20 | * Finds a token account and its associated mint account 21 | * by providing the token address. 22 | * 23 | * ```ts 24 | * const tokenWithMint = await metaplex.tokens().findTokenWithMintByAddress({ address }); 25 | * ``` 26 | * 27 | * @group Operations 28 | * @category Constructors 29 | */ 30 | export const findTokenWithMintByAddressOperation = 31 | useOperation(Key); 32 | 33 | /** 34 | * @group Operations 35 | * @category Types 36 | */ 37 | export type FindTokenWithMintByAddressOperation = Operation< 38 | typeof Key, 39 | FindTokenWithMintByAddressInput, 40 | TokenWithMint 41 | >; 42 | 43 | /** 44 | * @group Operations 45 | * @category Inputs 46 | */ 47 | export type FindTokenWithMintByAddressInput = { 48 | /** The address of the token account. */ 49 | address: PublicKey; 50 | }; 51 | 52 | /** 53 | * @group Operations 54 | * @category Handlers 55 | */ 56 | export const findTokenWithMintByAddressOperationHandler: OperationHandler = 57 | { 58 | handle: async ( 59 | operation: FindTokenWithMintByAddressOperation, 60 | metaplex: Metaplex, 61 | scope: OperationScope 62 | ): Promise => { 63 | const { commitment } = scope; 64 | const { address } = operation.input; 65 | 66 | const tokenAccount = toTokenAccount( 67 | await metaplex.rpc().getAccount(address, commitment) 68 | ); 69 | 70 | const mintAccount = toMintAccount( 71 | await metaplex.rpc().getAccount(tokenAccount.data.mint, commitment) 72 | ); 73 | 74 | return toTokenWithMint(tokenAccount, toMint(mintAccount)); 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './approveTokenDelegateAuthority'; 2 | export * from './createMint'; 3 | export * from './createToken'; 4 | export * from './createTokenWithMint'; 5 | export * from './findMintByAddress'; 6 | export * from './findTokenByAddress'; 7 | export * from './findTokenWithMintByAddress'; 8 | export * from './findTokenWithMintByMint'; 9 | export * from './freezeTokens'; 10 | export * from './mintTokens'; 11 | export * from './revokeTokenDelegateAuthority'; 12 | export * from './sendTokens'; 13 | export * from './thawTokens'; 14 | -------------------------------------------------------------------------------- /packages/js/src/plugins/tokenModule/program.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ASSOCIATED_TOKEN_PROGRAM_ID, 3 | TOKEN_PROGRAM_ID, 4 | } from '@solana/spl-token'; 5 | import { Program } from '@/types'; 6 | 7 | /** @group Programs */ 8 | export const tokenProgram: Program = { 9 | name: 'TokenProgram', 10 | address: TOKEN_PROGRAM_ID, 11 | }; 12 | 13 | /** @group Programs */ 14 | export const associatedTokenProgram: Program = { 15 | name: 'AssociatedTokenProgram', 16 | address: ASSOCIATED_TOKEN_PROGRAM_ID, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/utilsModule/UtilsClient.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex } from '@/Metaplex'; 2 | import { 3 | addAmounts, 4 | lamports, 5 | multiplyAmount, 6 | SolAmount, 7 | subtractAmounts, 8 | } from '@/types'; 9 | 10 | const TRANSACTION_FEE = 5000; 11 | 12 | /** 13 | * @group Modules 14 | */ 15 | export class UtilsClient { 16 | protected readonly metaplex: Metaplex; 17 | protected cachedRentPerEmptyAccount: SolAmount | null = null; 18 | protected cachedRentPerByte: SolAmount | null = null; 19 | 20 | constructor(metaplex: Metaplex) { 21 | this.metaplex = metaplex; 22 | } 23 | 24 | async estimate( 25 | bytes: number, 26 | numberOfAccounts = 1, 27 | numberOfTransactions = 1, 28 | useCache = true 29 | ): Promise { 30 | const rent = await this.estimateRent(bytes, numberOfAccounts, useCache); 31 | const transactionFees = this.estimateTransactionFee(numberOfTransactions); 32 | 33 | return addAmounts(rent, transactionFees); 34 | } 35 | 36 | async estimateRent( 37 | bytes: number, 38 | numberOfAccounts = 1, 39 | useCache = true 40 | ): Promise { 41 | if ( 42 | !useCache || 43 | this.cachedRentPerEmptyAccount === null || 44 | this.cachedRentPerByte === null 45 | ) { 46 | const rentFor0Bytes = await this.metaplex.rpc().getRent(0); 47 | 48 | // TODO(loris): Infer from header size in bytes. 49 | const rentFor1Byte = await this.metaplex.rpc().getRent(1); 50 | this.cachedRentPerEmptyAccount = rentFor0Bytes; 51 | this.cachedRentPerByte = subtractAmounts(rentFor1Byte, rentFor0Bytes); 52 | } 53 | 54 | const rentForAccounts = multiplyAmount( 55 | this.cachedRentPerEmptyAccount, 56 | numberOfAccounts 57 | ); 58 | const rentForBytes = multiplyAmount(this.cachedRentPerByte, bytes); 59 | 60 | return addAmounts(rentForAccounts, rentForBytes); 61 | } 62 | 63 | estimateTransactionFee(numberOfTransactions = 1): SolAmount { 64 | // TODO(loris): Improve with an RPC call to get the current transaction fee. 65 | return lamports(numberOfTransactions * TRANSACTION_FEE); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/js/src/plugins/utilsModule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UtilsClient'; 2 | export * from './plugin'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/utilsModule/plugin.ts: -------------------------------------------------------------------------------- 1 | import { UtilsClient } from './UtilsClient'; 2 | import type { Metaplex } from '@/Metaplex'; 3 | import { MetaplexPlugin } from '@/types'; 4 | 5 | /** @group Plugins */ 6 | export const utilsModule = (): MetaplexPlugin => ({ 7 | install(metaplex: Metaplex) { 8 | const utilsClient = new UtilsClient(metaplex); 9 | metaplex.utils = () => utilsClient; 10 | }, 11 | }); 12 | 13 | declare module '../../Metaplex' { 14 | interface Metaplex { 15 | utils(): UtilsClient; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/js/src/plugins/walletAdapterIdentity/WalletAdapterIdentityDriver.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Transaction } from '@solana/web3.js'; 2 | import { IdentityDriver } from '../identityModule'; 3 | import { 4 | OperationNotSupportedByWalletAdapterError, 5 | UninitializedWalletAdapterError, 6 | } from '@/errors'; 7 | 8 | export type WalletAdapter = { 9 | publicKey: PublicKey | null; 10 | signMessage?: (message: Uint8Array) => Promise; 11 | signTransaction?: (transaction: Transaction) => Promise; 12 | signAllTransactions?: (transactions: Transaction[]) => Promise; 13 | }; 14 | 15 | export class WalletAdapterIdentityDriver implements IdentityDriver { 16 | public readonly walletAdapter: WalletAdapter; 17 | 18 | constructor(walletAdapter: WalletAdapter) { 19 | this.walletAdapter = walletAdapter; 20 | } 21 | 22 | get publicKey(): PublicKey { 23 | if (!this.walletAdapter.publicKey) { 24 | throw new UninitializedWalletAdapterError(); 25 | } 26 | 27 | return this.walletAdapter.publicKey; 28 | } 29 | 30 | public async signMessage(message: Uint8Array): Promise { 31 | if (this.walletAdapter.signMessage === undefined) { 32 | throw new OperationNotSupportedByWalletAdapterError('signMessage'); 33 | } 34 | 35 | return this.walletAdapter.signMessage(message); 36 | } 37 | 38 | public async signTransaction(transaction: Transaction): Promise { 39 | if (this.walletAdapter.signTransaction === undefined) { 40 | throw new OperationNotSupportedByWalletAdapterError('signTransaction'); 41 | } 42 | 43 | return this.walletAdapter.signTransaction(transaction); 44 | } 45 | 46 | public async signAllTransactions( 47 | transactions: Transaction[] 48 | ): Promise { 49 | if (this.walletAdapter.signAllTransactions === undefined) { 50 | throw new OperationNotSupportedByWalletAdapterError( 51 | 'signAllTransactions' 52 | ); 53 | } 54 | 55 | return this.walletAdapter.signAllTransactions(transactions); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/js/src/plugins/walletAdapterIdentity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin'; 2 | export * from './WalletAdapterIdentityDriver'; 3 | -------------------------------------------------------------------------------- /packages/js/src/plugins/walletAdapterIdentity/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WalletAdapterIdentityDriver, 3 | WalletAdapter, 4 | } from './WalletAdapterIdentityDriver'; 5 | import { Metaplex } from '@/Metaplex'; 6 | import { MetaplexPlugin } from '@/types'; 7 | 8 | export const walletAdapterIdentity = ( 9 | walletAdapter: WalletAdapter 10 | ): MetaplexPlugin => ({ 11 | install(metaplex: Metaplex) { 12 | metaplex 13 | .identity() 14 | .setDriver(new WalletAdapterIdentityDriver(walletAdapter)); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/js/src/types/BigNumber.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer } from 'buffer'; 2 | import BN from 'bn.js'; 3 | import { default as assert } from '@/utils/assert'; 4 | import type { Opaque, Option } from '@/utils'; 5 | 6 | export type BigNumber = Opaque; 7 | export type BigNumberValues = 8 | | number 9 | | string 10 | | number[] 11 | | Uint8Array 12 | | Buffer 13 | | BN; 14 | 15 | export const toBigNumber = ( 16 | value: BigNumberValues, 17 | endian?: BN.Endianness 18 | ): BigNumber => { 19 | return new BN(value, endian) as BigNumber; 20 | }; 21 | 22 | export const toOptionBigNumber = ( 23 | value: Option 24 | ): Option => { 25 | return value === null ? null : toBigNumber(value); 26 | }; 27 | 28 | export const isBigNumber = (value: any): value is BigNumber => { 29 | return value?.__opaque__ === 'BigNumber'; 30 | }; 31 | 32 | export function assertBigNumber(value: any): asserts value is BigNumber { 33 | assert(isBigNumber(value), 'Expected BigNumber type'); 34 | } 35 | -------------------------------------------------------------------------------- /packages/js/src/types/Cluster.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | 3 | export type Cluster = 4 | | 'mainnet-beta' 5 | | 'devnet' 6 | | 'testnet' 7 | | 'localnet' 8 | | 'custom'; 9 | 10 | const MAINNET_BETA_DOMAINS = [ 11 | 'api.mainnet-beta.solana.com', 12 | 'ssc-dao.genesysgo.net', 13 | ]; 14 | const DEVNET_DOMAINS = [ 15 | 'api.devnet.solana.com', 16 | 'psytrbhymqlkfrhudd.dev.genesysgo.net', 17 | ]; 18 | const TESTNET_DOMAINS = ['api.testnet.solana.com']; 19 | const LOCALNET_DOMAINS = ['localhost', '127.0.0.1']; 20 | 21 | export const resolveClusterFromConnection = ( 22 | connection: Connection 23 | ): Cluster => { 24 | return resolveClusterFromEndpoint(connection.rpcEndpoint); 25 | }; 26 | 27 | export const resolveClusterFromEndpoint = (endpoint: string): Cluster => { 28 | const domain = new URL(endpoint).hostname; 29 | if (MAINNET_BETA_DOMAINS.includes(domain)) return 'mainnet-beta'; 30 | if (DEVNET_DOMAINS.includes(domain)) return 'devnet'; 31 | if (TESTNET_DOMAINS.includes(domain)) return 'testnet'; 32 | if (LOCALNET_DOMAINS.includes(domain)) return 'localnet'; 33 | return 'custom'; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/js/src/types/Creator.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { Signer } from './Signer'; 3 | 4 | /** 5 | * Object that represents the creator of an asset. 6 | * It contains its public key, its shares of the royalties in percent 7 | * and whether or not the creator is verified for a given asset 8 | * (i.e. they signed the asset). 9 | * 10 | * @group Models 11 | */ 12 | export type Creator = { 13 | /** The public key of the creator. */ 14 | readonly address: PublicKey; 15 | 16 | /** Whether or not the creator is verified for the asset. */ 17 | readonly verified: boolean; 18 | 19 | /** The creator's shares of the royalties in percent (i.e. 5 is 5%). */ 20 | readonly share: number; 21 | }; 22 | 23 | /** 24 | * This object provides a way of providing creator information when needed, 25 | * e.g. when creating or updating NFTs, candy machines, etc. 26 | * 27 | * It allows us to optionally provide an authority as a Signer so we can 28 | * both set and verify the creator within the same operation. 29 | * 30 | * @group Operations 31 | * @category Inputs 32 | */ 33 | export type CreatorInput = { 34 | /** The public key of the creator. */ 35 | readonly address: PublicKey; 36 | 37 | /** The creator's shares of the royalties in percent (i.e. 5 is 5%). */ 38 | readonly share: number; 39 | 40 | /** 41 | * The authority that should sign the asset to verify the creator. 42 | * When this is not provided, the creator will not be 43 | * verified within this operation. 44 | */ 45 | readonly authority?: Signer; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/js/src/types/DateTime.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { BigNumberValues } from './BigNumber'; 3 | import { default as assert } from '@/utils/assert'; 4 | import type { Opaque, Option } from '@/utils'; 5 | 6 | export type DateTimeString = string; 7 | export type DateTimeValues = DateTimeString | BigNumberValues | Date; 8 | export type DateTime = Opaque; 9 | 10 | export const toDateTime = (value: DateTimeValues): DateTime => { 11 | if (typeof value === 'string' || isDateObject(value)) { 12 | const date = new Date(value); 13 | const timestamp = Math.floor(date.getTime() / 1000); 14 | return new BN(timestamp) as DateTime; 15 | } 16 | 17 | return new BN(value) as DateTime; 18 | }; 19 | 20 | export const now = (): DateTime => toDateTime(new Date(Date.now())); 21 | 22 | export const toOptionDateTime = ( 23 | value: Option 24 | ): Option => { 25 | return value === null ? null : toDateTime(value); 26 | }; 27 | 28 | export const isDateTime = (value: any): value is DateTime => { 29 | return value?.__opaque__ === 'DateTime'; 30 | }; 31 | 32 | export function assertDateTime(value: any): asserts value is DateTime { 33 | assert(isDateTime(value), 'Expected DateTime type'); 34 | } 35 | 36 | const isDateObject = (value: any): value is Date => { 37 | return Object.prototype.toString.call(value) === '[object Date]'; 38 | }; 39 | 40 | export const formatDateTime = ( 41 | value: DateTime, 42 | // @ts-ignore 43 | locales: Intl.LocalesArgument = 'en-US', 44 | // @ts-ignore 45 | options: Intl.DateTimeFormatOptions = { 46 | month: 'short', 47 | day: 'numeric', 48 | year: 'numeric', 49 | hour: 'numeric', 50 | minute: 'numeric', 51 | } 52 | ): string => { 53 | const date = new Date(value.toNumber() * 1000); 54 | 55 | return date.toLocaleDateString(locales, options); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/js/src/types/FeatureFlags.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | 3 | export type FeatureFlags = boolean[]; 4 | 5 | /** 6 | * Serializes an array of boolean into a Buffer. The `byteSize` parameter 7 | * can be used to create a fixed-size Buffer, otherwise the Buffer will 8 | * have the minimum amount of bytes required to store the boolean array. 9 | * 10 | * Returns a Buffer whose bits are ordered from left to right, unless 11 | * `backward` is set to true, in which case the bits are ordered from 12 | * right to left. 13 | */ 14 | export const serializeFeatureFlags = ( 15 | features: FeatureFlags, 16 | byteSize?: number, 17 | backward = false 18 | ): Buffer => { 19 | byteSize = byteSize ?? Math.ceil(features.length / 8); 20 | const bytes: number[] = []; 21 | 22 | for (let i = 0; i < byteSize; i++) { 23 | let byte = 0; 24 | for (let j = 0; j < 8; j++) { 25 | const feature = Number(features[i * 8 + j] ?? 0); 26 | byte |= feature << (backward ? j : 7 - j); 27 | } 28 | if (backward) { 29 | bytes.unshift(byte); 30 | } else { 31 | bytes.push(byte); 32 | } 33 | } 34 | 35 | return Buffer.from(bytes); 36 | }; 37 | 38 | /** 39 | * Parses a Buffer into an array of booleans using the 40 | * bits of the buffer. The number of flags can be provided 41 | * to determine how many booleans to return. 42 | * 43 | * Expects the bits in the Buffer to be ordered from left to right, 44 | * unless `backward` is set to true, we expect the bits to be 45 | * ordered from right to left. 46 | */ 47 | export const deserializeFeatureFlags = ( 48 | buffer: Buffer, 49 | numberOfFlags?: number, 50 | backward = false 51 | ): FeatureFlags => { 52 | const booleans: boolean[] = []; 53 | buffer = backward ? buffer.reverse() : buffer; 54 | 55 | for (let byte of buffer) { 56 | for (let i = 0; i < 8; i++) { 57 | if (backward) { 58 | booleans.push(Boolean(byte & 1)); 59 | byte >>= 1; 60 | } else { 61 | booleans.push(Boolean(byte & 0b1000_0000)); 62 | byte <<= 1; 63 | } 64 | } 65 | } 66 | 67 | return booleans.slice(0, numberOfFlags); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/js/src/types/HasDriver.ts: -------------------------------------------------------------------------------- 1 | export type HasDriver = { 2 | driver: () => T; 3 | setDriver: (newDriver: T) => void; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/js/src/types/HasMetaplex.ts: -------------------------------------------------------------------------------- 1 | import { Metaplex } from '@/Metaplex'; 2 | 3 | export type HasMetaplex = { 4 | readonly metaplex: Metaplex; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/js/src/types/MetaplexPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex } from '@/Metaplex'; 2 | 3 | export type MetaplexPlugin = { 4 | install(metaplex: Metaplex): void; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/js/src/types/Model.ts: -------------------------------------------------------------------------------- 1 | import { default as assert } from '@/utils/assert'; 2 | 3 | /** 4 | * A helper type that defines a model as an opaque type. 5 | */ 6 | export type Model = { 7 | /** A model identifier to distinguish models in the SDK. */ 8 | readonly model: T; 9 | }; 10 | 11 | /** 12 | * A helper function that determines whether a value is a model 13 | * of the given type. 14 | */ 15 | export const isModel = , T extends string = M['model']>( 16 | model: T, 17 | value: any 18 | ): value is M => typeof value === 'object' && value.model === model; 19 | 20 | /** 21 | * A helper function to use in type guards asserting that a value is a model. 22 | * This currently wraps the `assert` method which is not exposed by the library. 23 | * In the future, we might replace this with a custom error. 24 | */ 25 | export function assertModel( 26 | condition: boolean, 27 | message?: string 28 | ): asserts condition { 29 | assert(condition, message); 30 | } 31 | -------------------------------------------------------------------------------- /packages/js/src/types/Pda.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { PublicKey, PublicKeyInitData } from '@solana/web3.js'; 3 | 4 | export class Pda extends PublicKey { 5 | /** The bump used to generate the PDA. */ 6 | public readonly bump: number; 7 | 8 | constructor(value: PublicKeyInitData, bump: number) { 9 | super(value); 10 | this.bump = bump; 11 | } 12 | 13 | static find(programId: PublicKey, seeds: Array): Pda { 14 | const [publicKey, bump] = PublicKey.findProgramAddressSync( 15 | seeds, 16 | programId 17 | ); 18 | 19 | return new Pda(publicKey, bump); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/js/src/types/Program.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { Metaplex } from '@/Metaplex'; 3 | import { Cluster } from '@/types'; 4 | import { GpaBuilder } from '@/utils'; 5 | 6 | export type ErrorWithLogs = Error & { logs: string[] }; 7 | export type ErrorWithCode = Error & { code: number }; 8 | 9 | export const isErrorWithLogs = (error: unknown): error is ErrorWithLogs => 10 | error instanceof Error && 'logs' in error; 11 | 12 | export type Program = { 13 | name: string; 14 | address: PublicKey; 15 | clusterFilter?: (cluster: Cluster) => boolean; 16 | errorResolver?: (error: ErrorWithLogs) => ErrorWithCode | null | undefined; 17 | gpaResolver?: (metaplex: Metaplex) => GpaBuilder; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/js/src/types/PublicKey.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, PublicKeyInitData } from '@solana/web3.js'; 2 | 3 | export { PublicKey } from '@solana/web3.js'; 4 | export type PublicKeyString = string; 5 | export type PublicKeyValues = 6 | | PublicKeyInitData 7 | | { publicKey: PublicKey } 8 | | { address: PublicKey }; 9 | 10 | export const toPublicKey = (value: PublicKeyValues): PublicKey => { 11 | if (typeof value === 'object' && 'publicKey' in value) { 12 | return value.publicKey; 13 | } 14 | 15 | if (typeof value === 'object' && 'address' in value) { 16 | return (value as { address: PublicKey }).address; 17 | } 18 | 19 | return new PublicKey(value); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/js/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Account'; 2 | export * from './Amount'; 3 | export * from './BigNumber'; 4 | export * from './Cluster'; 5 | export * from './Creator'; 6 | export * from './DateTime'; 7 | export * from './FeatureFlags'; 8 | export * from './HasDriver'; 9 | export * from './HasMetaplex'; 10 | export * from './MetaplexPlugin'; 11 | export * from './Model'; 12 | export * from './Operation'; 13 | export * from './Pda'; 14 | export * from './Program'; 15 | export * from './PublicKey'; 16 | export * from './Serializer'; 17 | export * from './Signer'; 18 | export * from './ReadApi'; 19 | -------------------------------------------------------------------------------- /packages/js/src/utils/Disposable.ts: -------------------------------------------------------------------------------- 1 | import EventEmitterPackage from 'eventemitter3'; 2 | import type EventEmitter from 'eventemitter3'; 3 | 4 | export type DisposableScope = { 5 | signal: AbortSignal | undefined; 6 | isCanceled: () => boolean; 7 | getCancelationError: () => unknown; 8 | throwIfCanceled: () => void; 9 | }; 10 | 11 | export class Disposable { 12 | protected eventEmitter: EventEmitter; 13 | protected signal: AbortSignal; 14 | protected cancelationError: unknown = null; 15 | protected abortListener: (error: unknown) => void; 16 | 17 | constructor(signal: AbortSignal) { 18 | this.signal = signal; 19 | this.eventEmitter = new EventEmitterPackage.EventEmitter(); 20 | this.abortListener = (error: unknown) => { 21 | this.cancelationError = error; 22 | this.eventEmitter.emit('cancel', error); 23 | this.close(); 24 | }; 25 | this.signal.addEventListener('abort', this.abortListener); 26 | } 27 | 28 | async run( 29 | callback: (scope: DisposableScope) => T, 30 | thenCloseDisposable = true 31 | ) { 32 | try { 33 | return await Promise.resolve(callback(this.getScope())); 34 | } finally { 35 | if (thenCloseDisposable) { 36 | this.close(); 37 | } 38 | } 39 | } 40 | 41 | getScope(): DisposableScope { 42 | return { 43 | signal: this.signal, 44 | isCanceled: () => this.isCanceled(), 45 | getCancelationError: () => this.cancelationError, 46 | throwIfCanceled: () => { 47 | if (this.isCanceled()) { 48 | throw this.getCancelationError(); 49 | } 50 | }, 51 | }; 52 | } 53 | 54 | isCanceled() { 55 | return this.signal.aborted; 56 | } 57 | 58 | getCancelationError() { 59 | return this.cancelationError; 60 | } 61 | 62 | onCancel(callback: (reason: unknown) => unknown): Disposable { 63 | this.eventEmitter.on('cancel', callback); 64 | 65 | return this; 66 | } 67 | 68 | close() { 69 | this.signal.removeEventListener('abort', this.abortListener); 70 | this.eventEmitter.removeAllListeners(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/js/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import { MetaplexError } from '../errors'; 2 | 3 | /** 4 | * Error indicating that an assertion failed. 5 | * @group Errors 6 | */ 7 | export class AssertionError extends Error { 8 | constructor(message: string) { 9 | super(message); 10 | this.name = 'AssertionError'; 11 | if (Error.captureStackTrace) { 12 | Error.captureStackTrace(this, this.constructor); 13 | } 14 | } 15 | } 16 | 17 | /** 18 | * Assserts that the provided condition is true. 19 | * @internal 20 | */ 21 | export default function assert( 22 | condition: boolean, 23 | message?: string 24 | ): asserts condition { 25 | if (!condition) { 26 | throw new AssertionError(message ?? 'Assertion failed'); 27 | } 28 | } 29 | 30 | /** 31 | * Asserts that both values are strictly equal. 32 | * @internal 33 | */ 34 | assert.equal = function assertEqual( 35 | actual: unknown, 36 | expected: T, 37 | message?: string 38 | ): asserts actual is T { 39 | if (actual !== expected) { 40 | throw new AssertionError((message ?? '') + ` ${actual} !== ${expected}`); 41 | } 42 | }; 43 | 44 | /** 45 | * Asserts that a given object contains the specified 46 | * keys such that their values are defined. 47 | */ 48 | export function assertObjectHasDefinedKeys< 49 | T extends object, 50 | K extends keyof T = keyof T 51 | >( 52 | input: T, 53 | keys: K[], 54 | onError: (missingKeys: K[]) => MetaplexError 55 | ): asserts input is { [key in keyof T]: T[key] } & { [key in K]-?: T[key] } { 56 | const missingKeys = keys.filter( 57 | (property) => input?.[property] === undefined 58 | ); 59 | 60 | if (missingKeys.length > 0) { 61 | throw onError(missingKeys); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/js/src/utils/exports.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This method is necessary to import certain packages on both ESM and CJS modules. 3 | * Without this, we get a different structure on each module. For instance: 4 | * - CJS: { default: [Getter], WebBundlr: [Getter] } 5 | * - ESM: { default: { default: [Getter], WebBundlr: [Getter] } } 6 | * This method fixes this by ensure there is not double default in the imported package. 7 | */ 8 | // eslint-disable-next-line @typescript-eslint/naming-convention 9 | export function _removeDoubleDefault(pkg: T): T { 10 | if ( 11 | pkg && 12 | typeof pkg === 'object' && 13 | 'default' in pkg && 14 | 'default' in (pkg as any).default 15 | ) { 16 | return (pkg as any).default; 17 | } 18 | 19 | return pkg; 20 | } 21 | -------------------------------------------------------------------------------- /packages/js/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assert'; 2 | export * from './common'; 3 | export * from './Disposable'; 4 | export * from './exports'; 5 | export * from './GmaBuilder'; 6 | export * from './GpaBuilder'; 7 | export * from './log'; 8 | export * from './merkle'; 9 | export * from './readApiConnection'; 10 | export * from './Task'; 11 | export * from './TransactionBuilder'; 12 | export * from './types'; 13 | 14 | /** @internal */ 15 | export { default as assert } from './assert'; 16 | -------------------------------------------------------------------------------- /packages/js/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import debug from 'debug'; 3 | 4 | export const logErrorDebug = debug('mp-sdk:error'); 5 | export const logInfoDebug = debug('mp-sdk:info'); 6 | export const logDebug = debug('mp-sdk:debug'); 7 | export const logTrace = debug('mp-sdk:trace'); 8 | 9 | export const logError = logErrorDebug.enabled 10 | ? logErrorDebug 11 | : console.error.bind(console); 12 | export const logInfo = logInfoDebug.enabled 13 | ? logInfoDebug 14 | : console.log.bind(console); 15 | -------------------------------------------------------------------------------- /packages/js/src/utils/merkle.ts: -------------------------------------------------------------------------------- 1 | import { MerkleTree } from 'merkletreejs'; 2 | import { keccak_256 } from '@noble/hashes/sha3'; 3 | 4 | /** 5 | * Describes the required data input for 6 | * handling Merkle Tree operations. 7 | */ 8 | type MerkleTreeInput = Uint8Array | string; 9 | 10 | /** 11 | * Creates a Merkle Tree from the provided data. 12 | */ 13 | export const getMerkleTree = (data: MerkleTreeInput[]): MerkleTree => { 14 | return new MerkleTree(data.map(keccak_256), keccak_256, { 15 | sortPairs: true, 16 | }); 17 | }; 18 | 19 | /** 20 | * Creates a Merkle Root from the provided data. 21 | * 22 | * This root provides a short identifier for the 23 | * provided data that is unique and deterministic. 24 | * This means, we can use this root to verify that 25 | * a given data is part of the original data set. 26 | */ 27 | export const getMerkleRoot = (data: MerkleTreeInput[]): Uint8Array => { 28 | return getMerkleTree(data).getRoot(); 29 | }; 30 | 31 | /** 32 | * Creates a Merkle Proof for a given data item. 33 | * 34 | * This proof can be used to verify that the given 35 | * data item is part of the original data set. 36 | */ 37 | export const getMerkleProof = ( 38 | data: MerkleTreeInput[], 39 | leaf: MerkleTreeInput, 40 | index?: number 41 | ): Uint8Array[] => { 42 | return getMerkleTree(data) 43 | .getProof(Buffer.from(keccak_256(leaf)), index) 44 | .map((proofItem) => proofItem.data); 45 | }; 46 | 47 | /** 48 | * Creates a Merkle Proof for a data item at a given index. 49 | * 50 | * This proof can be used to verify that the data item at 51 | * the given index is part of the original data set. 52 | */ 53 | export const getMerkleProofAtIndex = ( 54 | data: MerkleTreeInput[], 55 | index: number 56 | ): Uint8Array[] => { 57 | return getMerkleProof(data, data[index], index); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/js/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type PartialKeys = Omit< 2 | T, 3 | K 4 | > & 5 | Partial>; 6 | 7 | export type RequiredKeys = Omit< 8 | T, 9 | K 10 | > & 11 | Required>; 12 | 13 | export type Option = T | null; 14 | 15 | export type Opaque = T & { __opaque__: K }; 16 | -------------------------------------------------------------------------------- /packages/js/test/cjs-export.test.cjs: -------------------------------------------------------------------------------- 1 | const { test } = require('tape'); 2 | const { Connection } = require('@solana/web3.js'); 3 | const { LOCALHOST } = require('@metaplex-foundation/amman-client'); 4 | const exported = require('../dist/cjs/index.cjs'); 5 | 6 | test('[cjs] it successfully exports commonjs named exports', (t) => { 7 | const exportedKeys = Object.keys(exported); 8 | 9 | t.ok(exportedKeys.includes('Metaplex')); 10 | t.end(); 11 | }); 12 | 13 | test('[cjs] it can import the Irys client', async (t) => { 14 | const { IrysStorageDriver, Metaplex } = exported; 15 | const connection = new Connection(LOCALHOST); 16 | const metaplex = new Metaplex(connection); 17 | const irysDriver = new IrysStorageDriver(metaplex); 18 | const irys = await irysDriver.irys(); 19 | t.ok(typeof irys === 'object', 'Irys is an object'); 20 | t.ok('uploader' in irys, 'Irys can upload'); 21 | t.ok('getLoadedBalance' in irys, 'Irys can get the loaded balance'); 22 | t.ok('fund' in irys, 'Irys can fund'); 23 | t.ok('withdrawBalance' in irys, 'Irys can withdraw'); 24 | t.end(); 25 | }); 26 | 27 | test('[cjs] it can import Merkle helpers', async (t) => { 28 | const { getMerkleRoot, getMerkleProof } = exported; 29 | const data = ['a', 'b', 'c', 'd', 'e']; 30 | const merkleRoot = getMerkleRoot(data); 31 | const merkleProof = getMerkleProof(data, 'a'); 32 | t.ok(typeof merkleRoot === 'object', 'Merkle Root is an object'); 33 | t.ok(Array.isArray(merkleProof), 'Merkle Proof is an array'); 34 | t.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/js/test/esm-export.test.mjs: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { Connection } from '@solana/web3.js'; 3 | import { LOCALHOST } from '@metaplex-foundation/amman-client'; 4 | import * as exported from '../dist/esm/index.mjs'; 5 | 6 | test('[esm] it successfully exports esm named exports', (t) => { 7 | const exportedKeys = Object.keys(exported); 8 | 9 | t.ok(exportedKeys.includes('Metaplex')); 10 | t.end(); 11 | }); 12 | 13 | test('[esm] it can import the Irys client', async (t) => { 14 | const { IrysStorageDriver, Metaplex } = exported; 15 | const connection = new Connection(LOCALHOST); 16 | const metaplex = new Metaplex(connection); 17 | const irysDriver = new IrysStorageDriver(metaplex); 18 | const irys = await irysDriver.irys(); 19 | t.ok(typeof irys === 'object', 'Irys is an object'); 20 | t.ok('uploader' in irys, 'Irys can upload'); 21 | t.ok('getLoadedBalance' in irys, 'Irys can get the loaded balance'); 22 | t.ok('fund' in irys, 'Irys can fund'); 23 | t.ok('withdrawBalance' in irys, 'Irys can withdraw'); 24 | t.end(); 25 | }); 26 | 27 | test('[esm] it can import Merkle helpers', async (t) => { 28 | const { getMerkleRoot, getMerkleProof } = exported; 29 | const data = ['a', 'b', 'c', 'd', 'e']; 30 | const merkleRoot = getMerkleRoot(data); 31 | const merkleProof = getMerkleProof(data, 'a'); 32 | t.ok(typeof merkleRoot === 'object', 'Merkle Root is an object'); 33 | t.ok(Array.isArray(merkleProof), 'Merkle Proof is an array'); 34 | t.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/js/test/helpers/amman.ts: -------------------------------------------------------------------------------- 1 | import { Amman } from '@metaplex-foundation/amman-client'; 2 | import { 3 | PROGRAM_ADDRESS as TOKEN_METADATA_ADDRESS, 4 | cusper as cusperTokenMetadata, 5 | } from '@metaplex-foundation/mpl-token-metadata'; 6 | import { TransactionError } from '@solana/web3.js'; 7 | import { logDebug } from '../../src/utils/log'; 8 | 9 | export const amman = Amman.instance({ 10 | knownLabels: { [TOKEN_METADATA_ADDRESS]: 'Token Metadata' }, 11 | log: logDebug, 12 | }); 13 | 14 | // ----------------- 15 | // Amman Utils 16 | // ----------------- 17 | 18 | // Most of the below should get added to amman or cusper proper 19 | 20 | type TransactionInstructionError = { 21 | InstructionError: [number, { Custom: number }]; 22 | }; 23 | 24 | function isTransactionInstructionError( 25 | error: TransactionError | TransactionInstructionError 26 | ): error is TransactionInstructionError { 27 | return (error as TransactionInstructionError).InstructionError != null; 28 | } 29 | 30 | export function errorCode(err: TransactionError | TransactionInstructionError) { 31 | if (isTransactionInstructionError(err)) { 32 | return err.InstructionError[1].Custom; 33 | } 34 | } 35 | export function resolveTransactionError( 36 | cusper: typeof cusperTokenMetadata, 37 | err: TransactionError | TransactionInstructionError 38 | ) { 39 | const code = errorCode(err); 40 | if (code == null) { 41 | return new Error(`Unknown error ${err}`); 42 | } 43 | const cusperError = cusper.errorFromCode(code); 44 | if (cusperError == null) { 45 | return new Error(`Unknown error ${err} with code ${code}`); 46 | } 47 | 48 | return cusperError; 49 | } 50 | 51 | export function maybeThrowError( 52 | cusper: typeof cusperTokenMetadata, 53 | err: TransactionError | TransactionInstructionError | null | undefined 54 | ) { 55 | if (err == null) return; 56 | throw resolveTransactionError(cusper, err); 57 | } 58 | -------------------------------------------------------------------------------- /packages/js/test/helpers/assets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bench_80x80, 3 | bridge_80x80, 4 | rock_80x80, 5 | walrus_80x80, 6 | } from '../fixtures/images'; 7 | 8 | export const rockPng = Buffer.from(rock_80x80, 'base64'); 9 | export const bridgePng = Buffer.from(bridge_80x80, 'base64'); 10 | export const walrusPng = Buffer.from(walrus_80x80, 'base64'); 11 | export const benchPng = Buffer.from(bench_80x80, 'base64'); 12 | -------------------------------------------------------------------------------- /packages/js/test/helpers/consts.ts: -------------------------------------------------------------------------------- 1 | /** @private id to use for mock storage (matches the one defined in ammanrc) */ 2 | export const MOCK_STORAGE_ID = 'js-next-sdk'; 3 | -------------------------------------------------------------------------------- /packages/js/test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './amman'; 2 | export * from './asserts'; 3 | export * from './assets'; 4 | export * from './consts'; 5 | export * from './setup'; 6 | export * from './utils'; 7 | -------------------------------------------------------------------------------- /packages/js/test/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { ConfirmOptions } from '@solana/web3.js'; 2 | import test from 'tape'; 3 | 4 | /** 5 | * This is a workaround the fact that web3.js doesn't close it's socket connection and provides no way to do so. 6 | * Therefore the process hangs for a considerable time after the tests finish, increasing the feedback loop. 7 | * 8 | * This fixes this by exiting the process as soon as all tests are finished. 9 | */ 10 | export function killStuckProcess() { 11 | // Don't do this in CI since we need to ensure we get a non-zero exit code if tests fail 12 | if (process.env.CI == null) { 13 | test.onFinish(() => process.exit(0)); 14 | } 15 | } 16 | 17 | export const SKIP_PREFLIGHT: ConfirmOptions = { 18 | skipPreflight: true, 19 | commitment: 'confirmed', 20 | }; 21 | -------------------------------------------------------------------------------- /packages/js/test/plugins/auctionHouseModule/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Metaplex } from '@/Metaplex'; 2 | import { CreateAuctionHouseInput } from '@/plugins'; 3 | import { sol, Signer, OperationOptions } from '@/types'; 4 | 5 | export const createAuctionHouse = async ( 6 | mx: Metaplex, 7 | auctioneerAuthority?: Signer | null, 8 | input: Partial = {}, 9 | options: OperationOptions = {} 10 | ) => { 11 | const { auctionHouse } = await mx.auctionHouse().create( 12 | { 13 | sellerFeeBasisPoints: 200, 14 | auctioneerAuthority: auctioneerAuthority?.publicKey, 15 | ...input, 16 | }, 17 | options 18 | ); 19 | 20 | await mx.rpc().airdrop(auctionHouse.feeAccountAddress, sol(100)); 21 | 22 | return auctionHouse; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/js/test/plugins/auctionHouseModule/withdrawFromTreasuryAccount.test.ts: -------------------------------------------------------------------------------- 1 | import test, { Test } from 'tape'; 2 | import { metaplex, killStuckProcess, createWallet } from '../../helpers'; 3 | import { createAuctionHouse } from './helpers'; 4 | import { sol, toPublicKey } from '@/types'; 5 | 6 | killStuckProcess(); 7 | 8 | test('[auctionHouseModule] withdraw from treasury account on an Auction House', async (t: Test) => { 9 | // Given we have an Auction House with fee that equals 10% and an NFT. 10 | const mx = await metaplex(); 11 | const payer = await createWallet(mx); 12 | const auctionHouse = await createAuctionHouse( 13 | mx, 14 | null, 15 | { sellerFeeBasisPoints: 1000 }, 16 | { payer } 17 | ); 18 | 19 | // And withdrawal destination has 100 SOL. 20 | const originalTreasuryWithdrawalDestinationBalance = await mx 21 | .rpc() 22 | .getBalance(toPublicKey(auctionHouse.treasuryWithdrawalDestinationAddress)); 23 | 24 | t.same( 25 | originalTreasuryWithdrawalDestinationBalance.basisPoints.toNumber(), 26 | sol(100).basisPoints.toNumber() 27 | ); 28 | 29 | // And we airdropped 2 SOL to the treasury account. 30 | await mx.rpc().airdrop(auctionHouse.treasuryAccountAddress, sol(2)); 31 | 32 | // When we withdraw 1 SOL from treasury account. 33 | await mx 34 | .auctionHouse() 35 | .withdrawFromTreasuryAccount({ auctionHouse, amount: sol(1) }, { payer }); 36 | 37 | // Then treasury account has 1 SOL in it. 38 | const treasuryBalance = await mx 39 | .rpc() 40 | .getBalance(auctionHouse.treasuryAccountAddress); 41 | 42 | t.same(treasuryBalance.basisPoints.toNumber(), sol(1).basisPoints.toNumber()); 43 | 44 | // And withdrawal destination account has 101 SOL after withdrawal. 45 | const treasuryWithdrawalDestinationBalance = await mx 46 | .rpc() 47 | .getBalance(toPublicKey(auctionHouse.treasuryWithdrawalDestinationAddress)); 48 | 49 | t.same( 50 | treasuryWithdrawalDestinationBalance.basisPoints.toNumber(), 51 | sol(101).basisPoints.toNumber() 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineModule/deleteCandyGuard.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test from 'tape'; 3 | import { killStuckProcess, metaplex } from '../../helpers'; 4 | import { createCandyGuard } from './helpers'; 5 | 6 | killStuckProcess(); 7 | 8 | test('[candyMachineModule] it can delete a Candy Guard', async (t) => { 9 | // Given an existing Candy Guard. 10 | const mx = await metaplex(); 11 | const candyGuardAuthority = Keypair.generate(); 12 | const candyGuard = await createCandyGuard(mx, { 13 | authority: candyGuardAuthority.publicKey, 14 | }); 15 | 16 | // When we delete the Candy Guard account. 17 | await mx.candyMachines().deleteCandyGuard({ 18 | candyGuard: candyGuard.address, 19 | authority: candyGuardAuthority, 20 | }); 21 | 22 | // Then the Candy Guard account no longer exists. 23 | t.false(await mx.rpc().accountExists(candyGuard.address)); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineModule/deleteCandyMachine.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test from 'tape'; 3 | import { killStuckProcess, metaplex } from '../../helpers'; 4 | import { createCandyMachine } from './helpers'; 5 | import { assert } from '@/index'; 6 | 7 | killStuckProcess(); 8 | 9 | test('[candyMachineModule] it can delete a Candy Machine', async (t) => { 10 | // Given an existing Candy Machine with a Candy Guard. 11 | const mx = await metaplex(); 12 | const candyMachineAuthority = Keypair.generate(); 13 | const { candyMachine } = await createCandyMachine(mx, { 14 | authority: candyMachineAuthority, 15 | }); 16 | assert(!!candyMachine.candyGuard, 'Candy Machine has a Candy Guard'); 17 | 18 | // When we delete the Candy Machine account. 19 | await mx.candyMachines().delete({ 20 | candyMachine: candyMachine.address, 21 | authority: candyMachineAuthority, 22 | }); 23 | 24 | // Then the Candy Machine account no longer exists. 25 | t.false(await mx.rpc().accountExists(candyMachine.address)); 26 | 27 | // But the Candy Guard account still exists. 28 | t.true(await mx.rpc().accountExists(candyMachine.candyGuard.address)); 29 | }); 30 | 31 | test('[candyMachineModule] it can delete a Candy Machine with its Candy Guard', async (t) => { 32 | // Given an existing Candy Machine with a Candy Guard. 33 | const mx = await metaplex(); 34 | const candyMachineAuthority = Keypair.generate(); 35 | const { candyMachine } = await createCandyMachine(mx, { 36 | authority: candyMachineAuthority, 37 | }); 38 | assert(!!candyMachine.candyGuard, 'Candy Machine has a Candy Guard'); 39 | 40 | // When we delete the Candy Machine account whilst specifying the Candy Guard. 41 | await mx.candyMachines().delete({ 42 | candyMachine: candyMachine.address, 43 | candyGuard: candyMachine.candyGuard.address, 44 | authority: candyMachineAuthority, 45 | }); 46 | 47 | // Then both the Candy Machine and Candy Guard accounts no longer exist. 48 | t.false(await mx.rpc().accountExists(candyMachine.address)); 49 | t.false(await mx.rpc().accountExists(candyMachine.candyGuard.address)); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineModule/findCandyGuardsByAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test from 'tape'; 3 | import { killStuckProcess, metaplex } from '../../helpers'; 4 | import { createCandyGuard } from './helpers'; 5 | 6 | killStuckProcess(); 7 | 8 | test('[candyMachineModule] it can fetch all candy guards from a given authority', async (t) => { 9 | // Given a Metaplex instance. 10 | const mx = await metaplex(); 11 | 12 | // And two Candy Guards from authority A. 13 | const authorityA = Keypair.generate().publicKey; 14 | const candyGuardA1 = await createCandyGuard(mx, { authority: authorityA }); 15 | const candyGuardA2 = await createCandyGuard(mx, { authority: authorityA }); 16 | 17 | // And one Candy Guard from authority B. 18 | const authorityB = Keypair.generate().publicKey; 19 | await createCandyGuard(mx, { authority: authorityB }); 20 | 21 | // When we fetch all Candy Guards from authority A. 22 | const candyGuards = await mx 23 | .candyMachines() 24 | .findAllCandyGuardsByAuthority({ authority: authorityA }); 25 | 26 | // Then we receive the two Candy Guards from authority A. 27 | t.equal(candyGuards.length, 2); 28 | t.same( 29 | candyGuards.map((c) => c.address.toBase58()).sort(), 30 | [candyGuardA1.address.toBase58(), candyGuardA2.address.toBase58()].sort() 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineModule/unwrapCandyGuard.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import spok, { Specifications } from 'spok'; 3 | import test from 'tape'; 4 | import { killStuckProcess, metaplex, spokSamePubkey } from '../../helpers'; 5 | import { createCandyMachine } from './helpers'; 6 | import { assert, CandyMachine } from '@/index'; 7 | 8 | killStuckProcess(); 9 | 10 | test('[candyMachineModule] it can unwrap a Candy Machine from Candy Guard', async (t) => { 11 | // Given a Candy Machine with a Candy Guard. 12 | const mx = await metaplex(); 13 | const candyMachineAuthority = Keypair.generate(); 14 | const { candyMachine } = await createCandyMachine(mx, { 15 | authority: candyMachineAuthority, 16 | }); 17 | t.notEqual(candyMachine.candyGuard, null); 18 | 19 | // When we unwrap the Candy Machine from its Candy Guard. 20 | assert(!!candyMachine.candyGuard, 'Candy Machine has a Candy Guard'); 21 | await mx.candyMachines().unwrapCandyGuard({ 22 | candyMachine: candyMachine.address, 23 | candyMachineAuthority, 24 | candyGuard: candyMachine.candyGuard.address, 25 | candyGuardAuthority: candyMachineAuthority, 26 | }); 27 | 28 | // Then the Candy Machine's data was updated accordingly. 29 | const updatedCandyMachine = await mx.candyMachines().refresh(candyMachine); 30 | 31 | spok(t, updatedCandyMachine, { 32 | $topic: 'Updated Candy Machine', 33 | model: 'candyMachine', 34 | address: spokSamePubkey(candyMachine.address), 35 | authorityAddress: spokSamePubkey(candyMachineAuthority.publicKey), 36 | mintAuthorityAddress: spokSamePubkey(candyMachineAuthority.publicKey), 37 | candyGuard: null, 38 | } as unknown as Specifications); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineModule/updateCandyGuardAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test from 'tape'; 3 | import { killStuckProcess, metaplex } from '../../helpers'; 4 | import { createCandyGuard } from './helpers'; 5 | 6 | killStuckProcess(); 7 | 8 | test('[candyMachineModule] it can update the authority of a Candy Guard account', async (t) => { 9 | // Given a Candy Guard account with an old authority. 10 | const mx = await metaplex(); 11 | const oldAuthority = Keypair.generate(); 12 | const candyGuard = await createCandyGuard(mx, { 13 | authority: oldAuthority.publicKey, 14 | }); 15 | 16 | // When we update its authority. 17 | const newAuthority = Keypair.generate(); 18 | await mx.candyMachines().updateCandyGuardAuthority({ 19 | candyGuard: candyGuard.address, 20 | authority: oldAuthority, 21 | newAuthority: newAuthority.publicKey, 22 | }); 23 | 24 | // Then the updated Candy Guard has the expected data. 25 | const updatedCandyGuard = await mx.candyMachines().refresh(candyGuard); 26 | t.ok( 27 | updatedCandyGuard.authorityAddress.equals(newAuthority.publicKey), 28 | 'The authority is updated' 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/js/test/plugins/candyMachineV2Module/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { sha512 } from '@noble/hashes/sha512'; 3 | import { amman } from '../../helpers'; 4 | import { 5 | CandyMachineV2Item, 6 | CreateCandyMachineV2Input, 7 | Metaplex, 8 | sol, 9 | toBigNumber, 10 | } from '@/index'; 11 | 12 | export async function createCandyMachineV2( 13 | mx: Metaplex, 14 | input: Partial & { 15 | items?: CandyMachineV2Item[]; 16 | } = {} 17 | ) { 18 | const candyMachineOutput = await mx.candyMachinesV2().create({ 19 | price: sol(1), 20 | sellerFeeBasisPoints: 500, 21 | itemsAvailable: toBigNumber(100), 22 | ...input, 23 | }); 24 | 25 | let { candyMachine } = candyMachineOutput; 26 | const { response } = candyMachineOutput; 27 | 28 | if (input.items) { 29 | await mx.candyMachinesV2().insertItems({ 30 | candyMachine, 31 | authority: mx.identity(), 32 | items: input.items, 33 | }); 34 | 35 | candyMachine = await mx.candyMachinesV2().refresh(candyMachine); 36 | } 37 | 38 | await amman.addr.addLabel('candy-machine', candyMachine.address); 39 | await amman.addr.addLabel('tx: create candy-machine', response.signature); 40 | 41 | return { 42 | response, 43 | candyMachine, 44 | }; 45 | } 46 | 47 | export function create32BitsHash( 48 | input: Buffer | string, 49 | slice?: number 50 | ): number[] { 51 | const hash = create32BitsHashString(input, slice); 52 | 53 | return Buffer.from(hash, 'utf8').toJSON().data; 54 | } 55 | 56 | export function create32BitsHashString( 57 | input: Buffer | string, 58 | slice = 32 59 | ): string { 60 | const hash = sha512(input).slice(0, slice / 2); 61 | 62 | return Buffer.from(hash).toString('hex'); 63 | } 64 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/approveNftCollectionAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import spok, { Specifications } from 'spok'; 3 | import test, { Test } from 'tape'; 4 | import { 5 | createCollectionNft, 6 | createNft, 7 | killStuckProcess, 8 | metaplex, 9 | spokSamePubkey, 10 | } from '../../helpers'; 11 | import { assertRefreshedCollectionHasSize } from './helpers'; 12 | import { Nft } from '@/index'; 13 | 14 | killStuckProcess(); 15 | 16 | test('[nftModule] it can approve a collection authority for a given NFT', async (t: Test) => { 17 | // Given a Metaplex instance. 18 | const mx = await metaplex(); 19 | 20 | // And an existing NFT with an unverified collection. 21 | const collectionAuthority = Keypair.generate(); 22 | const collection = await createCollectionNft(mx, { 23 | updateAuthority: collectionAuthority, 24 | }); 25 | const nft = await createNft(mx, { 26 | collection: collection.address, 27 | }); 28 | t.true(nft.collection, 'nft has a collection'); 29 | t.false(nft.collection?.verified, 'nft collection is not verified'); 30 | await assertRefreshedCollectionHasSize(t, mx, collection, 0); 31 | 32 | // When we approve a new delegated collection authority. 33 | const delegatedCollectionAuthority = Keypair.generate(); 34 | await mx.nfts().approveCollectionAuthority({ 35 | mintAddress: collection.address, 36 | collectionAuthority: delegatedCollectionAuthority.publicKey, 37 | updateAuthority: collectionAuthority, 38 | }); 39 | 40 | // Then that delegated authority can successfully verify the NFT. 41 | await mx.nfts().verifyCollection({ 42 | mintAddress: nft.address, 43 | collectionMintAddress: nft.collection!.address, 44 | collectionAuthority: delegatedCollectionAuthority, 45 | isDelegated: true, 46 | }); 47 | 48 | // And the NFT should now be verified. 49 | const updatedNft = await mx.nfts().refresh(nft); 50 | spok(t, updatedNft, { 51 | $topic: 'Updated Nft', 52 | model: 'nft', 53 | collection: { 54 | address: spokSamePubkey(collection.address), 55 | verified: true, 56 | }, 57 | } as unknown as Specifications); 58 | 59 | // And the collection should have the updated size. 60 | await assertRefreshedCollectionHasSize(t, mx, collection, 1); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/findNftsByOwner.test.ts: -------------------------------------------------------------------------------- 1 | import test, { Test } from 'tape'; 2 | import { metaplex, createNft, killStuckProcess } from '../../helpers'; 3 | import { Metadata } from '@/index'; 4 | 5 | killStuckProcess(); 6 | 7 | test('[nftModule] it can fetch all NFTs in a wallet', async (t: Test) => { 8 | // Given a metaplex instance and a connected wallet. 9 | const mx = await metaplex(); 10 | const owner = mx.identity().publicKey; 11 | 12 | // And two NFTs inside that wallets. 13 | const nftA = await createNft(mx, { name: 'NFT A' }); 14 | const nftB = await createNft(mx, { name: 'NFT B' }); 15 | 16 | // When we fetch all NFTs in the wallet. 17 | const nfts = (await mx.nfts().findAllByOwner({ owner })) as Metadata[]; 18 | 19 | // Then we get the right NFTs. 20 | t.same(nfts.map((nft) => nft.name).sort(), ['NFT A', 'NFT B']); 21 | t.same( 22 | nfts.map((nft) => nft.mintAddress.toBase58()).sort(), 23 | [nftA.address.toBase58(), nftB.address.toBase58()].sort() 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/findNftsByUpdateAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test, { Test } from 'tape'; 3 | import { metaplex, createNft, killStuckProcess } from '../../helpers'; 4 | import type { Metadata, Metaplex } from '@/index'; 5 | 6 | killStuckProcess(); 7 | 8 | const createNftWithAuthority = ( 9 | mx: Metaplex, 10 | name: string, 11 | updateAuthority: Keypair 12 | ) => createNft(mx, { name, updateAuthority }); 13 | 14 | test('[nftModule] it can fetch all NFTs for a given update authority', async (t: Test) => { 15 | // Given a metaplex instance and 2 wallet. 16 | const mx = await metaplex(); 17 | const walletA = Keypair.generate(); 18 | const walletB = Keypair.generate(); 19 | 20 | // Where wallet A is the update authority of NFT A and B but not C. 21 | const nftA = await createNftWithAuthority(mx, 'NFT A', walletA); 22 | const nftB = await createNftWithAuthority(mx, 'NFT B', walletA); 23 | await createNftWithAuthority(mx, 'NFT C', walletB); 24 | 25 | // When we fetch all NFTs where wallet A is the authority. 26 | const nfts = (await mx.nfts().findAllByUpdateAuthority({ 27 | updateAuthority: walletA.publicKey, 28 | })) as Metadata[]; 29 | 30 | // Then we get the right NFTs. 31 | t.same(nfts.map((nft) => nft.name).sort(), ['NFT A', 'NFT B']); 32 | t.same( 33 | nfts.map((nft) => nft.mintAddress.toBase58()).sort(), 34 | [nftA.address.toBase58(), nftB.address.toBase58()].sort() 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/freezeDelegatedNft.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountState } from '@solana/spl-token'; 2 | import { Keypair } from '@solana/web3.js'; 3 | import spok, { Specifications } from 'spok'; 4 | import test, { Test } from 'tape'; 5 | import { 6 | assertThrows, 7 | createNft, 8 | killStuckProcess, 9 | metaplex, 10 | } from '../../helpers'; 11 | import { NftWithToken } from '@/index'; 12 | 13 | killStuckProcess(); 14 | 15 | test('[nftModule] a delegated authority can freeze its NFT', async (t: Test) => { 16 | // Given an existing delegated NFT. 17 | const mx = await metaplex(); 18 | const delegateAuthority = Keypair.generate(); 19 | const nft = await createNft(mx); 20 | await mx.tokens().approveDelegateAuthority({ 21 | mintAddress: nft.address, 22 | delegateAuthority: delegateAuthority.publicKey, 23 | }); 24 | 25 | // When the delegated authority freezes the NFT. 26 | await mx 27 | .nfts() 28 | .freezeDelegatedNft({ mintAddress: nft.address, delegateAuthority }); 29 | 30 | // Then the token account for that NFT is frozen. 31 | const frozenNft = await mx.nfts().refresh(nft); 32 | spok(t, frozenNft, { 33 | model: 'nft', 34 | $topic: 'Frozen NFT', 35 | token: { 36 | state: AccountState.Frozen, 37 | }, 38 | } as unknown as Specifications); 39 | }); 40 | 41 | test('[nftModule] the owner of the NFT cannot freeze its own NFT without a delegated authority', async (t: Test) => { 42 | // Given an existing NFT that's not delegated. 43 | const mx = await metaplex(); 44 | const owner = Keypair.generate(); 45 | const nft = await createNft(mx, { tokenOwner: owner.publicKey }); 46 | 47 | // When the owner tries to freeze the NFT. 48 | const promise = mx.nfts().freezeDelegatedNft({ 49 | mintAddress: nft.address, 50 | delegateAuthority: owner, 51 | tokenOwner: owner.publicKey, 52 | }); 53 | 54 | // Then we expect an error. 55 | await assertThrows( 56 | t, 57 | promise, 58 | /InvalidDelegate: All tokens in this account have not been delegated to this user/ 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Test } from 'tape'; 2 | import { Metaplex, Nft } from '@/index'; 3 | 4 | export const assertCollectionHasSize = ( 5 | t: Test, 6 | collectionNft: Nft, 7 | expectedSize: number 8 | ) => { 9 | t.equal( 10 | collectionNft.collectionDetails?.size?.toNumber(), 11 | expectedSize, 12 | `collection NFT has the expected size: ${expectedSize}` 13 | ); 14 | }; 15 | 16 | export const assertRefreshedCollectionHasSize = async ( 17 | t: Test, 18 | mx: Metaplex, 19 | collectionNft: Nft, 20 | expectedSize: number 21 | ) => { 22 | const updateCollectionNft = await mx.nfts().refresh(collectionNft); 23 | assertCollectionHasSize(t, updateCollectionNft, expectedSize); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/lockNft.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TokenRecord, 3 | TokenStandard, 4 | TokenState, 5 | } from '@metaplex-foundation/mpl-token-metadata'; 6 | import { Keypair } from '@solana/web3.js'; 7 | import test, { Test } from 'tape'; 8 | import { createNft, killStuckProcess, metaplex } from '../../helpers'; 9 | 10 | killStuckProcess(); 11 | 12 | test('[nftModule] utility delegates can lock programmable NFTs', async (t: Test) => { 13 | // Given an existing PNFT. 14 | const mx = await metaplex(); 15 | const nftOwner = Keypair.generate(); 16 | const nft = await createNft(mx, { 17 | tokenStandard: TokenStandard.ProgrammableNonFungible, 18 | tokenOwner: nftOwner.publicKey, 19 | }); 20 | 21 | // And an approved utility delegate. 22 | const utilityDelegate = Keypair.generate(); 23 | await mx.nfts().delegate({ 24 | nftOrSft: nft, 25 | authority: nftOwner, 26 | delegate: { 27 | type: 'UtilityV1', 28 | delegate: utilityDelegate.publicKey, 29 | owner: nftOwner.publicKey, 30 | data: { amount: 1 }, 31 | }, 32 | }); 33 | 34 | // And the PNFT is unlocked. 35 | const tokenRecord = mx.nfts().pdas().tokenRecord({ 36 | mint: nft.address, 37 | token: nft.token.address, 38 | }); 39 | let tokenRecordAccount = await TokenRecord.fromAccountAddress( 40 | mx.connection, 41 | tokenRecord 42 | ); 43 | t.equal(tokenRecordAccount.state, TokenState.Unlocked); 44 | 45 | // When the utility delegate locks the PNFT. 46 | await mx.nfts().lock({ 47 | nftOrSft: nft, 48 | authority: { 49 | __kind: 'tokenDelegate', 50 | type: 'UtilityV1', 51 | delegate: utilityDelegate, 52 | owner: nftOwner.publicKey, 53 | }, 54 | }); 55 | 56 | // Then the PNFT has been locked. 57 | tokenRecordAccount = await TokenRecord.fromAccountAddress( 58 | mx.connection, 59 | tokenRecord 60 | ); 61 | t.equal(tokenRecordAccount.state, TokenState.Locked); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/revokeNftUseAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { UseMethod } from '@metaplex-foundation/mpl-token-metadata'; 2 | import { Keypair } from '@solana/web3.js'; 3 | import test, { Test } from 'tape'; 4 | import { 5 | assertThrows, 6 | createNft, 7 | killStuckProcess, 8 | metaplex, 9 | } from '../../helpers'; 10 | 11 | killStuckProcess(); 12 | 13 | test('[nftModule] it can revoke a Use authority for a given Nft', async (t: Test) => { 14 | // Given we have a Metaplex instance and a usable NFT. 15 | const mx = await metaplex(); 16 | const nft = await createNft(mx, { 17 | uses: { 18 | useMethod: UseMethod.Multiple, 19 | remaining: 10, 20 | total: 10, 21 | }, 22 | }); 23 | 24 | // And a use authority has been approved. 25 | const currentUser = Keypair.generate(); 26 | await mx.nfts().approveUseAuthority({ 27 | mintAddress: nft.address, 28 | user: currentUser.publicKey, 29 | }); 30 | 31 | // When we revoke that use authority. 32 | await mx.nfts().revokeUseAuthority({ 33 | mintAddress: nft.address, 34 | user: currentUser.publicKey, 35 | }); 36 | 37 | // Then it can no longer use that NFT. 38 | const promise = mx 39 | .nfts() 40 | .use({ mintAddress: nft.address, useAuthority: currentUser }); 41 | 42 | await assertThrows( 43 | t, 44 | promise, 45 | /The Use Authority Record is empty or already revoked/ 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/unlockNft.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TokenRecord, 3 | TokenStandard, 4 | TokenState, 5 | } from '@metaplex-foundation/mpl-token-metadata'; 6 | import { Keypair } from '@solana/web3.js'; 7 | import test, { Test } from 'tape'; 8 | import { createNft, killStuckProcess, metaplex } from '../../helpers'; 9 | 10 | killStuckProcess(); 11 | 12 | test('[nftModule] utility delegates can unlock programmable NFTs', async (t: Test) => { 13 | // Given an existing PNFT. 14 | const mx = await metaplex(); 15 | const nftOwner = Keypair.generate(); 16 | const nft = await createNft(mx, { 17 | tokenStandard: TokenStandard.ProgrammableNonFungible, 18 | tokenOwner: nftOwner.publicKey, 19 | }); 20 | 21 | // And an approved utility delegate. 22 | const utilityDelegate = Keypair.generate(); 23 | await mx.nfts().delegate({ 24 | nftOrSft: nft, 25 | authority: nftOwner, 26 | delegate: { 27 | type: 'UtilityV1', 28 | delegate: utilityDelegate.publicKey, 29 | owner: nftOwner.publicKey, 30 | data: { amount: 1 }, 31 | }, 32 | }); 33 | 34 | // And the PNFT is locked. 35 | await mx.nfts().lock({ 36 | nftOrSft: nft, 37 | authority: { 38 | __kind: 'tokenDelegate', 39 | type: 'UtilityV1', 40 | delegate: utilityDelegate, 41 | owner: nftOwner.publicKey, 42 | }, 43 | }); 44 | const tokenRecord = mx.nfts().pdas().tokenRecord({ 45 | mint: nft.address, 46 | token: nft.token.address, 47 | }); 48 | let tokenRecordAccount = await TokenRecord.fromAccountAddress( 49 | mx.connection, 50 | tokenRecord 51 | ); 52 | t.equal(tokenRecordAccount.state, TokenState.Locked); 53 | 54 | // When the utility delegate unlocks the PNFT. 55 | await mx.nfts().unlock({ 56 | nftOrSft: nft, 57 | authority: { 58 | __kind: 'tokenDelegate', 59 | type: 'UtilityV1', 60 | delegate: utilityDelegate, 61 | owner: nftOwner.publicKey, 62 | }, 63 | }); 64 | 65 | // Then the PNFT has been unlocked. 66 | tokenRecordAccount = await TokenRecord.fromAccountAddress( 67 | mx.connection, 68 | tokenRecord 69 | ); 70 | t.equal(tokenRecordAccount.state, TokenState.Unlocked); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/unverifyNftCreator.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import spok, { Specifications } from 'spok'; 3 | import test, { Test } from 'tape'; 4 | import { createNft, killStuckProcess, metaplex } from '../../helpers'; 5 | import { Nft } from '@/index'; 6 | 7 | killStuckProcess(); 8 | 9 | test('[nftModule] it can unverify a creator', async (t: Test) => { 10 | // Given we have a Metaplex instance. 11 | const mx = await metaplex(); 12 | 13 | // And an existing NFT with an verified creator. 14 | const creator = Keypair.generate(); 15 | const nft = await createNft(mx, { 16 | creators: [ 17 | { 18 | address: mx.identity().publicKey, 19 | share: 60, 20 | }, 21 | { 22 | address: creator.publicKey, 23 | authority: creator, 24 | share: 40, 25 | }, 26 | ], 27 | }); 28 | t.ok(nft.creators[0].verified, 'update authority is verified'); 29 | t.ok(nft.creators[1].verified, 'creator is verified'); 30 | 31 | // When we unverify the creator. 32 | await mx.nfts().unverifyCreator({ mintAddress: nft.address, creator }); 33 | 34 | // Then the returned NFT should have the updated data. 35 | const updatedNft = await mx.nfts().refresh(nft); 36 | spok(t, updatedNft, { 37 | $topic: 'Updated Nft', 38 | model: 'nft', 39 | creators: [ 40 | { 41 | address: mx.identity().publicKey, 42 | verified: true, 43 | share: 60, 44 | }, 45 | { 46 | address: creator.publicKey, 47 | verified: false, 48 | share: 40, 49 | }, 50 | ], 51 | } as unknown as Specifications); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/js/test/plugins/nftModule/verifyNftCreator.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import spok, { Specifications } from 'spok'; 3 | import test, { Test } from 'tape'; 4 | import { createNft, killStuckProcess, metaplex } from '../../helpers'; 5 | import { Nft } from '@/index'; 6 | 7 | killStuckProcess(); 8 | 9 | test('[nftModule] it can verify a creator', async (t: Test) => { 10 | // Given we have a Metaplex instance. 11 | const mx = await metaplex(); 12 | 13 | // And an existing NFT with an unverified creator. 14 | const creator = Keypair.generate(); 15 | const nft = await createNft(mx, { 16 | creators: [ 17 | { 18 | address: mx.identity().publicKey, 19 | share: 60, 20 | }, 21 | { 22 | address: creator.publicKey, 23 | share: 40, 24 | }, 25 | ], 26 | }); 27 | t.ok(nft.creators[0].verified, 'update authority is verified'); 28 | t.ok(!nft.creators[1].verified, 'creator is not verified'); 29 | 30 | // When we verify the creator. 31 | await mx.nfts().verifyCreator({ mintAddress: nft.address, creator }); 32 | 33 | // Then the returned NFT should have the updated data. 34 | const updatedNft = await mx.nfts().refresh(nft); 35 | spok(t, updatedNft, { 36 | $topic: 'Updated Nft', 37 | model: 'nft', 38 | creators: [ 39 | { 40 | address: mx.identity().publicKey, 41 | verified: true, 42 | share: 60, 43 | }, 44 | { 45 | address: creator.publicKey, 46 | verified: true, 47 | share: 40, 48 | }, 49 | ], 50 | } as unknown as Specifications); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/js/test/plugins/rpcModule/RpcClient.test.ts: -------------------------------------------------------------------------------- 1 | import test, { Test } from 'tape'; 2 | import { metaplex, killStuckProcess } from '../../helpers'; 3 | import { ParsedProgramError } from '@/index'; 4 | 5 | killStuckProcess(); 6 | 7 | test('[rpcModule] it parses program errors when sending transactions', async (t: Test) => { 8 | // Given a Metaplex instance using a CoreRpcDriver. 9 | const mx = await metaplex(); 10 | 11 | // When we try to create an NFT with a name that's too long. 12 | const promise = mx.nfts().create({ 13 | uri: 'http://example.com/nft', 14 | sellerFeeBasisPoints: 200, 15 | name: 'x'.repeat(100), // Name is too long. 16 | }); 17 | 18 | // Then we receive a parsed program error. 19 | try { 20 | await promise; 21 | t.fail('Expected a ParsedProgramError'); 22 | } catch (error) { 23 | t.ok(error instanceof ParsedProgramError); 24 | t.ok((error as ParsedProgramError).message.includes('Name too long')); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /packages/js/test/plugins/tokenModule/freezeTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountState } from '@solana/spl-token'; 2 | import { Keypair } from '@solana/web3.js'; 3 | import test, { Test } from 'tape'; 4 | import { assertThrows, killStuckProcess, metaplex } from '../../helpers'; 5 | import { refreshToken } from './helpers'; 6 | import { token } from '@/index'; 7 | 8 | killStuckProcess(); 9 | 10 | test('[tokenModule] the freeze authority can freeze any token account', async (t: Test) => { 11 | // Given a Metaplex instance and an existing token account. 12 | const mx = await metaplex(); 13 | const freezeAuthority = Keypair.generate(); 14 | const owner = Keypair.generate(); 15 | const { token: tokenWithMint } = await mx.tokens().createTokenWithMint({ 16 | owner: owner.publicKey, 17 | freezeAuthority: freezeAuthority.publicKey, 18 | }); 19 | 20 | // When the owner freezes its own account. 21 | await mx.tokens().freeze({ 22 | mintAddress: tokenWithMint.mint.address, 23 | tokenOwner: owner.publicKey, 24 | freezeAuthority, 25 | }); 26 | 27 | // Then the token account is frozen. 28 | const refreshedToken = await refreshToken(mx, tokenWithMint); 29 | t.equal(refreshedToken.state, AccountState.Frozen, 'token account is frozen'); 30 | }); 31 | 32 | test('[tokenModule] a frozen account cannot send tokens', async (t: Test) => { 33 | // Given a Metaplex instance and an existing token account. 34 | const mx = await metaplex(); 35 | const freezeAuthority = Keypair.generate(); 36 | const owner = Keypair.generate(); 37 | const { token: tokenWithMint } = await mx.tokens().createTokenWithMint({ 38 | owner: owner.publicKey, 39 | freezeAuthority: freezeAuthority.publicKey, 40 | initialSupply: token(42), 41 | }); 42 | 43 | // And that token account has been frozen. 44 | await mx.tokens().freeze({ 45 | mintAddress: tokenWithMint.mint.address, 46 | tokenOwner: owner.publicKey, 47 | freezeAuthority, 48 | }); 49 | 50 | // When we try to send tokens from the frozen account. 51 | const promise = mx.tokens().send({ 52 | mintAddress: tokenWithMint.mint.address, 53 | amount: token(10), 54 | fromOwner: owner, 55 | toOwner: Keypair.generate().publicKey, 56 | }); 57 | 58 | // Then we expect an error. 59 | await assertThrows(t, promise, /Error: Account is frozen/); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/js/test/plugins/tokenModule/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Test } from 'tape'; 2 | import { 3 | formatAmount, 4 | isEqualToAmount, 5 | SplTokenAmount, 6 | Token, 7 | TokenWithMint, 8 | } from '@/index'; 9 | import type { Metaplex } from '@/Metaplex'; 10 | 11 | export const assertTokenHasAmount = ( 12 | t: Test, 13 | token: Token | TokenWithMint, 14 | amount: SplTokenAmount 15 | ) => { 16 | t.true( 17 | isEqualToAmount(token.amount, amount), 18 | `token has amount: ${formatAmount(amount)}` 19 | ); 20 | }; 21 | 22 | export const assertRefreshedTokenHasAmount = async ( 23 | t: Test, 24 | metaplex: Metaplex, 25 | token: Token | TokenWithMint, 26 | amount: SplTokenAmount 27 | ) => { 28 | const refreshedToken = await refreshToken(metaplex, token); 29 | assertTokenHasAmount(t, refreshedToken, amount); 30 | }; 31 | 32 | export const refreshToken = ( 33 | metaplex: Metaplex, 34 | token: Token | TokenWithMint 35 | ) => { 36 | return metaplex.tokens().findTokenByAddress({ address: token.address }); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/js/test/plugins/tokenModule/revokeTokenDelegateAuthority.test.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from '@solana/web3.js'; 2 | import test, { Test } from 'tape'; 3 | import { assertThrows, killStuckProcess, metaplex } from '../../helpers'; 4 | import { token } from '@/index'; 5 | 6 | killStuckProcess(); 7 | 8 | test('[tokenModule] a token owner can revoke an existing token delegate authority', async (t: Test) => { 9 | // Given a Metaplex instance and an existing token account containing 42 tokens. 10 | const mx = await metaplex(); 11 | const owner = Keypair.generate(); 12 | const { token: tokenWithMint } = await mx.tokens().createTokenWithMint({ 13 | owner: owner.publicKey, 14 | initialSupply: token(42), 15 | }); 16 | 17 | // And an approved token delegate authority for 10 tokens. 18 | const delegateAuthority = Keypair.generate(); 19 | await mx.tokens().approveDelegateAuthority({ 20 | mintAddress: tokenWithMint.mint.address, 21 | delegateAuthority: delegateAuthority.publicKey, 22 | amount: token(10), 23 | owner, 24 | }); 25 | 26 | // When the token owner revoke that authority. 27 | await mx.tokens().revokeDelegateAuthority({ 28 | mintAddress: tokenWithMint.mint.address, 29 | owner, 30 | }); 31 | 32 | // Then the delegate authority cannot use anymore tokens. 33 | const newOwner = Keypair.generate(); 34 | const promise = mx.tokens().send({ 35 | mintAddress: tokenWithMint.mint.address, 36 | delegateAuthority, 37 | fromOwner: owner.publicKey, 38 | toOwner: newOwner.publicKey, 39 | amount: token(1), 40 | }); 41 | 42 | await assertThrows(t, promise, /Error: owner does not match/); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/js/test/plugins/tokenModule/thawTokens.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountState } from '@solana/spl-token'; 2 | import { Keypair } from '@solana/web3.js'; 3 | import test, { Test } from 'tape'; 4 | import { killStuckProcess, metaplex } from '../../helpers'; 5 | import { refreshToken } from './helpers'; 6 | 7 | killStuckProcess(); 8 | 9 | test('[tokenModule] the freeze authority can thaw any frozen token account', async (t: Test) => { 10 | // Given a Metaplex instance and an existing token account. 11 | const mx = await metaplex(); 12 | const freezeAuthority = Keypair.generate(); 13 | const owner = Keypair.generate(); 14 | const { token: tokenWithMint } = await mx.tokens().createTokenWithMint({ 15 | owner: owner.publicKey, 16 | freezeAuthority: freezeAuthority.publicKey, 17 | }); 18 | 19 | // And given that account has been frozen. 20 | await mx.tokens().freeze({ 21 | mintAddress: tokenWithMint.mint.address, 22 | tokenOwner: owner.publicKey, 23 | freezeAuthority, 24 | }); 25 | 26 | // When the freeze authority thaws the account. 27 | await mx.tokens().thaw({ 28 | mintAddress: tokenWithMint.mint.address, 29 | tokenOwner: owner.publicKey, 30 | freezeAuthority, 31 | }); 32 | 33 | // Then the token account is no longer frozen. 34 | const refreshedToken = await refreshToken(mx, tokenWithMint); 35 | t.equal( 36 | refreshedToken.state, 37 | AccountState.Initialized, 38 | 'token account is not frozen' 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/js/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "outDir": "../dist/test", 8 | "declarationDir": null, 9 | "declaration": false, 10 | "emitDeclarationOnly": false, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "dist/esm", 6 | "declarationDir": "dist/types", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /programs/mpl_auction_house.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_auction_house.so -------------------------------------------------------------------------------- /programs/mpl_candy_guard.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_candy_guard.so -------------------------------------------------------------------------------- /programs/mpl_candy_machine.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_candy_machine.so -------------------------------------------------------------------------------- /programs/mpl_candy_machine_core.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_candy_machine_core.so -------------------------------------------------------------------------------- /programs/mpl_token_auth_rules.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_token_auth_rules.so -------------------------------------------------------------------------------- /programs/mpl_token_metadata.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/mpl_token_metadata.so -------------------------------------------------------------------------------- /programs/solana_gateway_program.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metaplex-foundation/js/5bfbd36921d0299f5013a67e2aedd1ae6a6cb2de/programs/solana_gateway_program.so -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["packages/*/src"], 3 | "exclude": ["node_modules", "packages/*/node_modules"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "noEmit": false, 9 | "noEmitOnError": true, 10 | "emitDeclarationOnly": true, 11 | "paths": { 12 | "@/*": ["packages/js/src/*"] 13 | }, 14 | 15 | "target": "es2019", 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "allowJs": false, 19 | "skipLibCheck": true, 20 | 21 | "noUnusedLocals": true, 22 | "strictNullChecks": true, 23 | "noImplicitAny": true, 24 | "noImplicitThis": true, 25 | "noImplicitReturns": false, 26 | "strict": true, 27 | "isolatedModules": true, 28 | 29 | "resolveJsonModule": true, 30 | "esModuleInterop": true, 31 | "removeComments": false, 32 | "jsx": "preserve", 33 | "stripInternal": true 34 | }, 35 | "typedocOptions": { 36 | "entryPoints": ".", 37 | "entryPointStrategy": "packages", 38 | "plugin": ["./infra/typedoc-plugin.js"], 39 | "out": "docs", 40 | "readme": "README.md", 41 | "name": "API References", 42 | "includeVersion": true, 43 | "excludeNotDocumented": false, 44 | "sort": ["static-first", "required-first", "source-order"], 45 | "categoryOrder": [ 46 | "Constructors", 47 | "Types", 48 | "Inputs", 49 | "Outputs", 50 | "Contexts", 51 | "*" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "clean": { 5 | "outputs": [] 6 | }, 7 | "build": { 8 | "dependsOn": ["^build"], 9 | "outputs": ["dist/**"] 10 | }, 11 | "lint": { 12 | "outputs": [] 13 | }, 14 | "lint:fix": { 15 | "outputs": [] 16 | }, 17 | "test": { 18 | "dependsOn": ["build"], 19 | "outputs": [] 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------