├── .gitignore ├── .prettierrc ├── README.md ├── WINDEX.md ├── cli ├── .gitignore ├── README.MD ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── examples └── next-js │ ├── .env.template │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ └── settings.json │ ├── README.md │ ├── components │ └── Button.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ ├── index.tsx │ ├── metadata.tsx │ ├── mint.tsx │ ├── mints.tsx │ └── state.tsx │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json ├── jestconfig.json ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── arweave-uploader-tests.ts │ ├── minting-utils.test.ts │ └── windex-tests.ts ├── arweave-uploader.ts ├── index.ts ├── program-ids.ts ├── utils │ ├── connection-utils.ts │ ├── metadata-utils.ts │ ├── minting-utils.ts │ ├── pda-utils.ts │ └── state-utils.ts ├── windex.ts └── wonka.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | *.tsbuildinfo 4 | /coverage 5 | /build 6 | 7 | .DS_Store 8 | .env 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wonka JS 2 | 3 | `Wonka JS` is the easiest way to mint from [Candy Machine](https://docs.metaplex.com/candy-machine-v2/introduction) and fetch NFTs through JS APIs. You can see an end to end example in [Next.js demo project](https://github.com/wonka-labs/wonka-js/tree/main/examples/next-js) as well as debug using the [command line testing tool](https://github.com/wonka-labs/wonka-js/tree/main/cli). 4 | 5 | For using Wonka's Solana NFT indexing APIs, checkout [Windex Documentation](https://github.com/wonka-labs/wonka-js/blob/main/WINDEX.md). 6 | 7 | ![FI3xQ2FVcAQO3wK](https://user-images.githubusercontent.com/796815/153501801-7b3b5d27-a747-4df8-8cec-c5c7d2b233bb.jpeg) 8 | 9 | 10 | Once you have [followed the instructions to upload your NFTs](https://docs.metaplex.com/candy-machine-v2/preparing-assets), you can use functions below to build your mint flow: 11 | 12 | * `mintCandyMachineToken(..)` 13 | * `getCandyMachineMints(..)` 14 | * `getCandyMachineState(...)` 15 | * `getMintMetadata(...)` 16 | * `updateMintImage(...)` 17 | 18 | These commands are useful if you need to build a custom facing front end, and don't want to rely on the [Candy Machine Minting Site](https://docs.metaplex.com/candy-machine-v2/mint-frontend). 19 | 20 | ## Installation 21 | `npm install @wonka-labs/wonka-js` 22 | 23 | ## Wonka APIs 24 | 25 | ### Getting Machine State 26 | Returns info about currently available mints in Candy Machine, how many were already minted, how long is left for the auction, etc. 27 | 28 | ```JS 29 | const getCandyMachineState = async () => { 30 | console.log("Getting candy machine state.") 31 | const provider = ...; 32 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; 33 | const wonka = new Wonka(provider, candyMachineId) 34 | const candyMachineState = await wonka.getCandyMachineState() 35 | console.log(candyMachineState) 36 | } 37 | ``` 38 | 39 | ### Getting Existing Mints 40 | Returns a list of all existing mints on the given candy machine. 41 | 42 | ```JS 43 | const getCandyMachineMints = async() => { 44 | console.log("Getting candy machine mints...") 45 | const provider = ...; 46 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; 47 | const wonka = new Wonka(provider, candyMachineId) 48 | const candyMachineMints = await wonka.getCandyMachineMints() 49 | console.log(candyMachineMints) 50 | } 51 | ``` 52 | 53 | ### Minting a new NFT 54 | Mints an NFT; you either get an error with a message or the ID of the mint in return. 55 | 56 | ```JS 57 | const mintCandyMachineToken = async(recipientWallet: PublicKey) => { 58 | const provider = ...; 59 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; 60 | const wonka = new Wonka(provider, candyMachineId) 61 | const candyMachineMintId = await wonka.mintCandyMachineToken(recipientWallet) 62 | console.log(candyMachineMintId) 63 | } 64 | ``` 65 | 66 | ### Fetching Mint Metadata 67 | Sometimes you need to load one particular NFT's metadata, here is how you can do it: 68 | 69 | ```JS 70 | const getMintMetadata = async(mintAddress: string) => { 71 | const provider = ...; 72 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; 73 | const wonka = new Wonka(provider, candyMachineId) 74 | const mintMetadata = await wonka.getMintMetadata(mintAddress) 75 | console.log(`Fetched mint metadata:`); 76 | console.log(mintMetadata); 77 | 78 | // Can also fetch the data stored inside the metadata: 79 | const metadataDataURIData = await fetch(mintMetadata.uri); 80 | const metadataDataURIDataJSON = await metadataDataURIData.json(); 81 | console.log(`Fetched mint metadata's URI data:`); 82 | console.log(metadataDataURIDataJSON); 83 | } 84 | ``` 85 | 86 | ### Updating Mint Photo (Advanced) 87 | This is a bit more advanced, but if you have a wallet with the update authority, you can actually update the NFT's png. 88 | Since this requires access to arweave's private key, it's probably better to create a backend API that uses these functions. 89 | Here is what's happening at a high-level: 90 | 91 | 1. You create an ArweaveUploader with a private key that has enough money in it for uploading files. 92 | 2. You create a wonka with a provider that's attached to a wallet that has permission to update the NFT's metadata. 93 | 3. You give Wonka a base64 encoded PNG and a mint address. That's it! 94 | 95 | ```JS 96 | const updateMintImage = async(mintAddress: PublicKey, b64image: string) => { 97 | console.log("Getting mint metadata...") 98 | const provider = ...; 99 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; 100 | const wonka = new Wonka(provider, candyMachineId) 101 | const arweaveUploader = new ArweaveUploader(process.env.ARWEAVE_KEY!); 102 | const txn = await wonka.updateMintImage( 103 | b64image, 104 | arweaveUploader, 105 | provider.wallet, 106 | mintAddress, 107 | { image_updated: true } 108 | ); 109 | } 110 | ``` 111 | 112 | ### Storing Ids & Keys 113 | The question is where to store information like candy machine ID, etc. If you're using React or Next.js, you can easily use the .env file so that the code above looks more like: 114 | 115 | ```JS 116 | const candyMachineId = process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!; // For Next.js 117 | const candyMachineId = process.env.REACT_APP_CANDY_MACHINE_ID!; // For React 118 | ``` 119 | 120 | Read more about these in the docs: [React .env](https://create-react-app.dev/docs/adding-custom-environment-variables/) and [Next.js .env](https://nextjs.org/docs/basic-features/environment-variables). 121 | 122 | ## Windex APIs 123 | 124 | *⚠️ The Windex service is being deprecated as of 10/17/2022. After that, all responses will return errors. See [here](https://discord.com/channels/944635349009833985/953849529919238234/1024135909563699230) for more information and alternatives 125 | 126 | By default, fetching NFTs by Wallet, Collection, or ID requires fetching a series of Solana accounts and external JSON metadata, which can be slow and bandwidth intensive. The Wonka Index (windex) is a backend cache that enables blazing fast metadata fetches. You can use the following queries to easily fetch NFTs. For more details, visit [WINDEX.md](WINDEX.md). 127 | 128 | ### Fetching NFTs by Candy Machine or Collection ID 129 | 130 | To display all NFTs in a collection, you can query Windex by Candy Machine ID or Collection ID (the collection id is the first verified creator in the NFT's metadata). 131 | 132 | ```JS 133 | const fetchNFTsByCandyMachine = async(candyMachineId: PublicKey) => { 134 | const nfts = await Windex.fetchNFTsByCandyMachineID(candyMachineId, 20, Windex.DEVNET_ENDPOINT); 135 | console.log(`Retrieved ${nfts.length} NFTs!`); 136 | } 137 | 138 | const fetchNFTsByCollection = async(collectionId: PublicKey) => { 139 | const nfts = await Windex.fetchNFTsByCollectionID(collectionId, 20, Windex.DEVNET_ENDPOINT); 140 | console.log(`Retrieved ${nfts.length} NFTs!`); 141 | } 142 | ``` 143 | 144 | ### Fetching NFTs in a Wallet Address 145 | 146 | ```JS 147 | const fetchNFTsByWallet = async(walletAddress: PublicKey) => { 148 | const nfts = await Windex.fetchNFTsByWallet(walletAddress, 20, Windex.DEVNET_ENDPOINT); 149 | console.log(`Retrieved ${nfts.length} NFTs in ${walletAddress}'s wallet!`); 150 | } 151 | ``` 152 | 153 | ### Fetching an NFT by Mint Address 154 | 155 | ```JS 156 | const fetchNFTsByMintAddress = async(mintAddress: PublicKey) => { 157 | const nft = await Windex.fetchNFTByMintAddress(mintAddress, Windex.DEVNET_ENDPOINT); 158 | if (!nft) { 159 | console.log("nft not found!"); 160 | } else { 161 | console.log(`Fetched ${nft.address}: ${nft.name}`); 162 | } 163 | } 164 | ``` 165 | -------------------------------------------------------------------------------- /WINDEX.md: -------------------------------------------------------------------------------- 1 | *⚠️ The Windex service is being deprecated as of 10/17/2022. After that, all responses will return errors. See [here](https://discord.com/channels/944635349009833985/953849529919238234/1024135909563699230) for more information and alternatives.* 2 | 3 | # Windex 4 | 5 | The Wonka Index API (called "Windex") is a blazing fast API on top of Solana/Metaplex NFTs, to enable quick and performant fetching of NFTs by collection, wallet, and mint. 6 | 7 | The Wonka Index is currently in Alpha testing. Please join our [Discord](https://discord.gg/p2wXHT3vm7) to report issues or suggest new features, or open up a Github Issue. 8 | 9 | ![windex](https://user-images.githubusercontent.com/796815/157983069-284a79f0-2379-412a-a529-eaca36a2428b.jpeg) 10 | 11 | 12 | ## Javascript Library 13 | 14 | The easiest way to get started with Windex is using the `wonka` JS library. 15 | 16 | ### Installation 17 | ``` 18 | npm install @wonka-labs/wonka-js 19 | ``` 20 | 21 | ### Fetching NFTs by Candy Machine or Collection ID 22 | 23 | To display all NFTs in a collection, you can query Windex by Candy Machine ID or Collection ID (the collection id is the first verified creator in the NFT's metadata). 24 | 25 | ```JS 26 | import Windex from "@wonka-labs/wonka-js"; 27 | import { PublicKey } from '@solana/web3.js'; 28 | 29 | const fetchNFTsByCandyMachine = async(candyMachineId: PublicKey) => { 30 | const nfts = await Windex.fetchNFTsByCandyMachineID(candyMachineId, 20, Windex.DEVNET_ENDPOINT); 31 | console.log(`Retrieved ${nfts.length} NFTs!`); 32 | } 33 | 34 | const fetchNFTsByCollection = async(collectionId: PublicKey) => { 35 | const nfts = await Windex.fetchNFTsByCollectionID(collectionId, 20, Windex.DEVNET_ENDPOINT); 36 | console.log(`Retrieved ${nfts.length} NFTs!`); 37 | } 38 | ``` 39 | 40 | ### Fetching NFTs in a Wallet Address 41 | 42 | ```JS 43 | import Windex from "@wonka-labs/wonka-js"; 44 | import { PublicKey } from '@solana/web3.js'; 45 | 46 | const fetchNFTsByWallet = async(walletAddress: PublicKey) => { 47 | const nfts = await Windex.fetchNFTsByWallet(walletAddress, 20, Windex.DEVNET_ENDPOINT); 48 | console.log(`Retrieved ${nfts.length} NFTs in ${walletAddress}'s wallet!`); 49 | } 50 | ``` 51 | 52 | ### Fetching an NFT by Mint Address 53 | 54 | ```JS 55 | import Windex from "@wonka-labs/wonka-js"; 56 | import { PublicKey } from '@solana/web3.js'; 57 | 58 | const fetchNFTsByMintAddress = async(mintAddress: PublicKey) => { 59 | const nft = await Windex.fetchNFTByMintAddress(mintAddress, Windex.DEVNET_ENDPOINT); 60 | if (!nft) { 61 | console.log("nft not found!"); 62 | } else { 63 | console.log(`Fetched ${nft.address}: ${nft.name}`); 64 | } 65 | } 66 | ``` 67 | 68 | ### Fetching Sol Domain Name from Wallet Address 69 | 70 | ```JS 71 | import Windex from "@wonka-labs/wonka-js"; 72 | import { PublicKey } from '@solana/web3.js'; 73 | 74 | async function fetchSolDomainByWalletAddress(walletAddress: PublicKey) { 75 | const solDomainMetadata = await Windex.fetchSolDomainMetadataByAddress(walletAddress) 76 | console.log(solDomainMetadata) 77 | } 78 | ``` 79 | 80 | ### Fetching Wallet Address from Sol Domain 81 | 82 | ```JS 83 | import Windex from "@wonka-labs/wonka-js"; 84 | 85 | async function fetchWalletAddressBySolDomain(solDomain: string) { 86 | const solDomainMetadata = await Windex.fetchAddressBySolDomain( 87 | solDomain 88 | ); 89 | return solDomainMetadata?.address 90 | } 91 | ``` 92 | 93 | ### Fetching Twitter Handle from Sol Domain 94 | 95 | ```JS 96 | import Windex from "@wonka-labs/wonka-js"; 97 | 98 | async function fetchTwitterHandleBySolDomain(solDomain: string) { 99 | const solDomainMetadata = await Windex.fetchAddressBySolDomain( 100 | solDomain 101 | ); 102 | return solDomainMetadata?.twitter 103 | } 104 | ``` 105 | 106 | ## GraphQL API (Advanced) 107 | 108 | Alternatively, you can use the GraphQL API to access the exact fields you need. The easiest way to craft queries is through the [Windex interactive query editor](https://api.wonkalabs.xyz/v0.1/solana/graphiql?cluster=devnet). 109 | 110 | The main endpoints are `candyMachineV2(...)`, `nftsByWallet(...)`, `nftsByCollection(...)`, and `nftByMintID(...)`. Using the query editor, you can discover and query for all of the fields you need (from both off-chain and on-chain data). 111 | 112 | The GraphQL API is a typical HTTP POST request: 113 | 114 | ``` 115 | curl 'https://api.wonkalabs.xyz/v0.1/solana/mainnet/graphql' \ 116 | -H 'content-type: application/json' \ 117 | --data-raw '{"query":"query { solDomainNameForAddress(address:\"BYeHCJtokQecDkN34ZE4fWgF7U4vDtwjX6bkaiaprQmt\") }\n","variables":null}' 118 | ``` 119 | 120 | (There are plenty of [GraphQL client libraries](https://graphql.org/code/) that make it a little easier to enable complex GraphQL queries and responses in your frontend and backend code as well) 121 | 122 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | -------------------------------------------------------------------------------- /cli/README.MD: -------------------------------------------------------------------------------- 1 | # Wonka JS: Command Line Interface (CLI) 2 | 3 | The CLI comes in handy both as a tool to debug your candy machine as well as an easy way to test out the JS APIs. If you want to contribute, see [Installing from Source](#installing-from-source) 🙏. 4 | 5 | ![wonka-cli](https://user-images.githubusercontent.com/796815/152068325-b2581ffe-51ad-4d8b-8815-e8ac3a0eda36.png) 6 | 7 | 8 | ## Prerequisites 9 | 1. Install `node` and `npm` 10 | 2. [Setup a devnet wallet](https://docs.metaplex.com/candy-machine-v2/getting-started#setting-up-a-devnet-wallet-for-testing) — in examples below the key is saved in `~/.config/solana/devnet.json`. 11 | 3. [Create a candy machine](https://docs.metaplex.com/candy-machine-v2/creating-candy-machine) — save the candy machine ID somewhere handy to use in the commands. 12 | 13 | ## Installing 14 | ``` 15 | npm -g @triton-labs/wonka-cli 16 | ``` 17 | 18 | ## Commands 19 | Quick summary of the commands below: 20 | * [`get-state`](#getting-machine-state): prints out current candy machine's state. 21 | * [`get-mints`](#getting-existing-mints): prints out all existing mints from a particular candy machine. 22 | * [`mint`](#minting-a-new-nft): mints a new NFT and gives it to a recipient address. 23 | * [`get-metadata`](#fetching-mint-metadata): prints out all metadata associated with a candy machine mint. 24 | 25 | ### Getting Machine State 26 | Returns info about currently available mints in Candy Machine, how many were already minted, how long is left for the auction, etc. 27 | 28 | ```JS 29 | ➜ wonka-cli get-state -k ~/.config/solana/devnet.json -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb 30 | 31 | Loaded keypair with public key: 6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6. 32 | Initialized a Wonka with candy machine ID: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb. 33 | Fetched state for candy machine: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb: 34 | { 35 | itemsAvailable: 100, 36 | itemsRedeemed: 13, 37 | itemsRemaining: 87, 38 | goLiveData: 1640390400, 39 | goLiveDateTimeString: 'Sat, 25 Dec 2021 00:00:00 GMT' 40 | } 41 | ``` 42 | 43 | ### Getting Existing Mints 44 | Returns a list of all existing mints on the given candy machine. 45 | 46 | ```JS 47 | ➜ wonka-cli get-mints -k ~/.config/solana/devnet.json -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb 48 | 49 | Loaded keypair with public key: 6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6. 50 | Initialized a Wonka with candy machine ID: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb. 51 | Fetched all mints from candy machine: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb: 52 | [ 53 | Metadata { 54 | pubkey: PublicKey { 55 | _bn: 56 | }, 57 | info: { 58 | data: , 59 | executable: false, 60 | lamports: 5616720, 61 | owner: PublicKey { 62 | _bn: 63 | }, 64 | rentEpoch: 257 65 | }, 66 | data: MetadataData { 67 | key: 4, 68 | updateAuthority: '6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6', 69 | mint: '5XunTKCtqVeTuD1XxLrorP2x55UaR62SMekdnmA2UP1z', 70 | data: MetadataDataData { 71 | name: 'Wagmify PFP 85', 72 | symbol: '', 73 | uri: 'https://arweave.net/bhFGrMCP-GTKQJ5jOY6QKuCRoL5RLeQfWJIXzxnr0AQ', 74 | sellerFeeBasisPoints: 0, 75 | creators: [ 76 | Creator { 77 | address: '3CeEE1fnunVBtGLGkCfjxBnrfpE9UYJ7B1NRF8e7HKx6', 78 | verified: 1, 79 | share: 0 80 | }, 81 | Creator { 82 | address: '6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6', 83 | verified: 0, 84 | share: 100 85 | } 86 | ] 87 | }, 88 | primarySaleHappened: 1, 89 | isMutable: 1, 90 | editionNonce: 255, 91 | tokenStandard: undefined, 92 | collection: undefined, 93 | uses: undefined 94 | } 95 | }, 96 | ... 97 | ] 98 | ``` 99 | 100 | ### Minting a new NFT 101 | Mints an NFT; you either get an error with a message or the ID of the mint in return. 102 | 103 | ```JS 104 | ➜ wonka-cli mint \ 105 | -k ~/.config/solana/devnet.json \ 106 | -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb \ 107 | -r 6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6 108 | 109 | Loaded keypair with public key: 6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6. 110 | Initialized a Wonka with candy machine ID: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb. 111 | About to send transaction with all the candy instructions! 112 | Sent transaction with id: yw2a24eaqPjskRejqwZUGcL56xT8tJ7bjFajowNq7UvFX5w4HUkJDypU7HSR6z1i1WfUA5t3FNHYVpsMT8iwh3U for mint: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW. 113 | Got notification of type: status from txid: yw2a24eaqPjskRejqwZUGcL56xT8tJ7bjFajowNq7UvFX5w4HUkJDypU7HSR6z1i1WfUA5t3FNHYVpsMT8iwh3U. 114 | Minted 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW; waiting 30 seconds to fetch metadata... 115 | Loading metadata PDA FSZ4VEwFvZqwCEmMJ3215tMVBovkqCwyz2CSF9tZLo8T for token address: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW. 116 | Minted a new token: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW: 117 | MetadataDataData { 118 | name: 'Wagmify PFP 44', 119 | symbol: '', 120 | uri: 'https://arweave.net/mq_8uIkFJRB6JTt-nqkZPMYXz4I3n6uj68Twrx2FYtQ', 121 | sellerFeeBasisPoints: 0, 122 | creators: [ 123 | Creator { 124 | address: '3CeEE1fnunVBtGLGkCfjxBnrfpE9UYJ7B1NRF8e7HKx6', 125 | verified: 1, 126 | share: 0 127 | }, 128 | Creator { 129 | address: '6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6', 130 | verified: 0, 131 | share: 100 132 | } 133 | ] 134 | } 135 | ``` 136 | 137 | ### Fetching Mint Metadata 138 | Sometimes you need to load one particular NFT's metadata, here is how you can do it: 139 | 140 | ```JS 141 | ➜ wonka-cli get-metadata \ 142 | -k ~/.config/solana/devnet.json \ 143 | -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb \ 144 | -m 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW 145 | 146 | Loaded keypair with public key: 6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6. 147 | Initialized a Wonka with candy machine ID: Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb. 148 | Loading metadata PDA FSZ4VEwFvZqwCEmMJ3215tMVBovkqCwyz2CSF9tZLo8T for token address: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW. 149 | Fetched metadata for mint: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW: 150 | MetadataDataData { 151 | name: 'Wagmify PFP 44', 152 | symbol: '', 153 | uri: 'https://arweave.net/mq_8uIkFJRB6JTt-nqkZPMYXz4I3n6uj68Twrx2FYtQ', 154 | sellerFeeBasisPoints: 0, 155 | creators: [ 156 | Creator { 157 | address: '3CeEE1fnunVBtGLGkCfjxBnrfpE9UYJ7B1NRF8e7HKx6', 158 | verified: 1, 159 | share: 0 160 | }, 161 | Creator { 162 | address: '6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6', 163 | verified: 0, 164 | share: 100 165 | } 166 | ] 167 | } 168 | Fetched metadata URI data for mint: 9iTKkg9JuzoNZePjgfd1XxXcKQL9e1c4Zkf2pbDTj7eW: 169 | { 170 | name: 'Wagmify PFP 44', 171 | collection: { name: 'Wagmify Season 1', family: 'Wagmify' }, 172 | symbol: '', 173 | image: 'https://www.arweave.net/74ASD4jh5_frj3pNr2HSNwVEt_72Oz7jMeqm6R8KK_s?ext=png', 174 | properties: { 175 | files: [ 176 | { 177 | uri: 'https://www.arweave.net/74ASD4jh5_frj3pNr2HSNwVEt_72Oz7jMeqm6R8KK_s?ext=png', 178 | type: 'image/png' 179 | } 180 | ], 181 | creators: [ 182 | { 183 | address: '6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6', 184 | share: 100 185 | } 186 | ] 187 | } 188 | } 189 | ``` 190 | 191 | ## Installing from Source 192 | 1. `mkdir ~/triton-labs` 193 | 2. `cd ~/triton-labs` 194 | 3. `git clone git@github.com:TritonLabs/wonka.git` 195 | 4. `cd wonka/cli` 196 | 5. `npm i` 197 | 6. `npm build` 198 | 199 | You can now run the command either through `ts-node` or compile with `tsc` and then run with `node`. If you're actively developing, running `tsc --watch` would autocompile on save. Here is an example: 200 | 201 | ``` 202 | ➜ cli git:(main) ts-node src/index.ts get-mints -k ~/.config/solana/devnet.json -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb 203 | ``` 204 | 205 | or after compiling: 206 | 207 | ``` 208 | ➜ cli git:(main) tsc 209 | ➜ cli git:(main) node lib/index.js get-mints -k ~/.config/solana/devnet.json -cmid Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb 210 | 211 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wonka-labs/wonka-cli", 3 | "version": "1.0.2", 4 | "description": "CLI utilities built around Wonka APIs.", 5 | "main": "lib/index.js", 6 | "bin": "lib/index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 10 | "lint": "tslint -p tsconfig.json", 11 | "prepare": "npm run build", 12 | "prepublishOnly": "npm run lint", 13 | "preversion": "npm run lint", 14 | "version": "npm run format && git add -A src", 15 | "postversion": "git push && git push --tags" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/wonka-labs/wonka-js", 20 | "directory": "cli" 21 | }, 22 | "keywords": [ 23 | "wonka", 24 | "nft", 25 | "solana", 26 | "candy", 27 | "machine", 28 | "mint", 29 | "metaplex" 30 | ], 31 | "author": "@0xZoZoZo", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/TritonLabs/wonka/issues" 35 | }, 36 | "homepage": "https://github.com/TritonLabs/wonka#readme", 37 | "dependencies": { 38 | "@project-serum/anchor": "^0.18.2", 39 | "@triton-labs/wonka": "^1.0.7", 40 | "commander": "^9.0.0", 41 | "install": "^0.13.0", 42 | "loglevel": "^1.8.0", 43 | "node-fetch": "^2.6.7", 44 | "npm": "^8.4.0", 45 | "typescript": "^4.5.5", 46 | "util": "^0.12.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import log from 'loglevel'; 5 | import util from 'util'; 6 | import { clusterApiUrl, Cluster, PublicKey, Connection, Keypair } from '@solana/web3.js'; 7 | import Wonka from '@triton-labs/wonka'; 8 | import fs from 'fs'; 9 | import { Provider, Wallet } from '@project-serum/anchor'; 10 | import fetch from 'node-fetch'; 11 | 12 | program.version('1.1.0'); 13 | log.setLevel('info'); 14 | 15 | programCommand('get-mints') 16 | .option('-cmid, --candy-machine-id ', 'Candy Machine ID.') 17 | .action(async (_, cmd) => { 18 | const { keypair, env, candyMachineId } = cmd.opts(); 19 | const wonka = wonkaWithCommandOptions(keypair, env, candyMachineId); 20 | const mints = await wonka.getCandyMachineMints(); 21 | prettyPrint(`Fetched all mints from candy machine: ${candyMachineId}:`, mints); 22 | }); 23 | 24 | programCommand('get-state') 25 | .option('-cmid, --candy-machine-id ', 'Candy Machine ID.') 26 | .action(async (_, cmd) => { 27 | const { keypair, env, candyMachineId } = cmd.opts(); 28 | const wonka = wonkaWithCommandOptions(keypair, env, candyMachineId); 29 | const state = await wonka.getCandyMachineState(); 30 | prettyPrint(`Fetched state for candy machine: ${candyMachineId}:`, state); 31 | }); 32 | 33 | programCommand('get-metadata') 34 | .option('-cmid, --candy-machine-id ', 'Candy Machine ID.') 35 | .option('-m, --mint ', 'base58 mint key') 36 | .action(async (_, cmd) => { 37 | const { keypair, env, candyMachineId, mint } = cmd.opts(); 38 | const wonka = wonkaWithCommandOptions(keypair, env, candyMachineId); 39 | const mintAddress = new PublicKey(mint); 40 | const mintMetadata = await wonka.getMintMetadata(mintAddress); 41 | const metadataDataURIData = await fetch(mintMetadata.data.data.uri); 42 | const metadataDataURIDataJSON = await metadataDataURIData.json(); 43 | prettyPrint(`Fetched metadata for mint: ${mint}:`, mintMetadata); 44 | prettyPrint(`Fetched metadata URI data for mint: ${mint}:`, metadataDataURIDataJSON); 45 | }); 46 | 47 | programCommand('mint') 48 | .option('-cmid, --candy-machine-id ', 'Candy Machine ID.') 49 | .option('-r, --recipient ', 'base58 recipient public address') 50 | .action(async (_, cmd) => { 51 | const { keypair, env, candyMachineId, recipient } = cmd.opts(); 52 | const wonka = wonkaWithCommandOptions(keypair, env, candyMachineId); 53 | const recipientWalletAddress = new PublicKey(recipient); 54 | const { mintAddress, txid, error, errorMessage } = await wonka.mintCandyMachineToken(recipientWalletAddress); 55 | if (error) { 56 | prettyPrint('Failed to mint with error:', error); 57 | prettyPrint('Transaction id: ', txid); 58 | prettyPrint('Error message: ', errorMessage); 59 | } else { 60 | log.info(`Minted ${mintAddress!}; waiting 30 seconds to fetch metadata...`); 61 | setTimeout(async () => { 62 | const mintMetadata = await wonka.getMintMetadata(mintAddress!); 63 | prettyPrint(`Minted a new token: ${mintAddress}:`, mintMetadata); 64 | }, 30 * 1000); 65 | } 66 | }); 67 | 68 | function wonkaWithCommandOptions(keypairFile: string, env: Cluster, candyMachineId: string): Wonka { 69 | const connection = new Connection(clusterApiUrl(env)); 70 | const loadedKeypair = loadKeypair(keypairFile); 71 | const payerWallet = new Wallet(loadedKeypair); 72 | const provider = new Provider(connection, payerWallet, { 73 | commitment: 'processed', 74 | }); 75 | return new Wonka(provider, candyMachineId); 76 | } 77 | 78 | function loadKeypair(keypairFile): Keypair { 79 | if (!keypairFile || keypairFile === '') { 80 | throw new Error('Keypair is required!'); 81 | } 82 | const keypair = Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(keypairFile).toString()))); 83 | log.info(`Loaded keypair with public key: ${keypair.publicKey}.`); 84 | return keypair; 85 | } 86 | 87 | function programCommand(name: string) { 88 | return program 89 | .command(name) 90 | .option( 91 | '-e, --env ', 92 | 'Solana cluster env name', 93 | 'devnet', // mainnet-beta, testnet, devnet 94 | ) 95 | .option('-k, --keypair ', `Solana wallet location`, '--keypair not provided') 96 | .option('-l, --log-level ', 'log level', setLogLevel); 97 | } 98 | 99 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 100 | function setLogLevel(value) { 101 | if (value === undefined || value === null) { 102 | return; 103 | } 104 | log.info('setting the log value to: ' + value); 105 | log.setLevel(value); 106 | } 107 | 108 | function prettyPrint(description: string, obj: any) { 109 | log.info(description); 110 | log.info(util.inspect(obj, { colors: true, depth: 6 })); 111 | } 112 | 113 | program.parse(process.argv); 114 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "sourceMap": true, 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "**/__tests__/*"] 14 | } 15 | -------------------------------------------------------------------------------- /examples/next-js/.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CANDY_MACHINE_ID=Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb 2 | -------------------------------------------------------------------------------- /examples/next-js/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next-js/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /examples/next-js/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.quickSuggestions": { 4 | "strings": true 5 | }, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/CVS": true, 11 | "**/.DS_Store": true, 12 | "**/Thumbs.db": true, 13 | "**/node_modules": true 14 | } 15 | } -------------------------------------------------------------------------------- /examples/next-js/README.md: -------------------------------------------------------------------------------- 1 | # Wonka JS — Next.js Example 2 | The example gives you concrete code snippets for authenticating the user and using Wonka JS to mint tokens on Metaplex's Candy Machine. 3 | 4 | ![next](https://user-images.githubusercontent.com/796815/152266612-65372a86-adc1-4099-aa60-10c0ee5416c2.png) 5 | 6 | ## Getting Started 7 | 8 | 1. Git clone this repo, then `cd wonka/examples/next-example` 9 | 2. Run `mv .env.template .env`, then vim `.env` to set your `NEXT_PUBLIC_CANDY_MACHINE_ID`. 10 | 3. Run `npm install` 11 | 4. Run `npm run dev` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | ## Included Examples 18 | 19 | 1. `state.tsx` — fetches state related to the candy machine. 20 | 2. `mint.tsx` — mints a new NFT. 21 | 3. `mints.tsx` — fetches all mints from candy machine. 22 | 4. `metadata.tsx` — fetches all metadata from a particular mint. 23 | 24 | ## About Next.js 25 | 26 | To learn more about Next.js, take a look at the following resources: 27 | 28 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 29 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 30 | 31 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!` 32 | -------------------------------------------------------------------------------- /examples/next-js/components/Button.tsx: -------------------------------------------------------------------------------- 1 | export enum ButtonStyle { 2 | Small = 1, 3 | Large, 4 | } 5 | 6 | const Button = ({ title, didTapButton }: { title: string; didTapButton: () => void }) => { 7 | return ( 8 | <> 9 | 13 | {title} 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Button; 20 | -------------------------------------------------------------------------------- /examples/next-js/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next-js/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ['www.arweave.net', 'windex-api.onrender.com'], 6 | }, 7 | webpack: (config, { isServer }) => { 8 | if (!isServer) { 9 | config.resolve.fallback.fs = false 10 | } 11 | return config 12 | }, 13 | } 14 | 15 | module.exports = nextConfig 16 | -------------------------------------------------------------------------------- /examples/next-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-example", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@metaplex-foundation/mpl-token-metadata": "^1.1.0", 12 | "@metaplex/js": "^4.10.0", 13 | "@solana/wallet-adapter-base": "^0.9.2", 14 | "@solana/wallet-adapter-react": "^0.15.2", 15 | "@solana/wallet-adapter-react-ui": "^0.9.4", 16 | "@solana/wallet-adapter-wallets": "^0.14.2", 17 | "@solana/web3.js": "^1.32.0", 18 | "@triton-labs/wonka": "file:../..", 19 | "fs": "^0.0.1-security", 20 | "next": "12.0.9", 21 | "react": "^17.0.2", 22 | "react-dom": "17.0.2", 23 | "react-toastify": "^8.1.0", 24 | "spinners-react": "^1.0.6", 25 | "tailwindcss": "^3.0.18" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "17.0.13", 29 | "@types/react": "17.0.38", 30 | "eslint": "8.8.0", 31 | "eslint-config-next": "12.0.9", 32 | "typescript": "4.5.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/next-js/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 2 | import { 3 | ConnectionProvider, 4 | WalletProvider, 5 | } from "@solana/wallet-adapter-react"; 6 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; 7 | import Head from "next/head"; 8 | import { 9 | LedgerWalletAdapter, 10 | PhantomWalletAdapter, 11 | SlopeWalletAdapter, 12 | SolflareWalletAdapter, 13 | SolletExtensionWalletAdapter, 14 | SolletWalletAdapter, 15 | TorusWalletAdapter, 16 | } from "@solana/wallet-adapter-wallets"; 17 | import { clusterApiUrl } from "@solana/web3.js"; 18 | import { AppProps } from "next/app"; 19 | import { FC, useMemo } from "react"; 20 | 21 | require("../styles/globals.css"); 22 | require("@solana/wallet-adapter-react-ui/styles.css"); 23 | 24 | const CANDY_MACHINE_ID = process.env.NEXT_PUBLIC_CANDY_MACHINE_ID; 25 | 26 | const App: FC = ({ Component, pageProps }) => { 27 | const network = WalletAdapterNetwork.Devnet; 28 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 29 | const wallets = useMemo( 30 | () => [ 31 | new PhantomWalletAdapter(), 32 | new SolflareWalletAdapter(), 33 | new SlopeWalletAdapter(), 34 | new TorusWalletAdapter(), 35 | new LedgerWalletAdapter(), 36 | new SolletWalletAdapter({ network }), 37 | new SolletExtensionWalletAdapter({ network }), 38 | ], 39 | [network] 40 | ); 41 | 42 | const CandyMachineIdSetupInstructions = () => { 43 | return ( 44 |
45 |

Set Candy Machine ID

46 |

47 | Save your candy machine ID inside .env under a variable called{' '} 48 | NEXT_PUBLIC_CANDY_MACHINE_ID You can use .env.template as a starter. 49 |

50 |

51 | Once you have set it, please restart the app since Next.js doesn{"'"}t fast refresh .env variables). 52 |

53 |
54 | ); 55 | }; 56 | if (CANDY_MACHINE_ID == undefined) { 57 | return () 58 | } 59 | return ( 60 | <> 61 | 62 | Wonka Example 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /examples/next-js/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/next-js/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Button from '../components/Button'; 3 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 4 | import { useWallet } from "@solana/wallet-adapter-react"; 5 | import { useEffect } from 'react'; 6 | import { useRouter } from 'next/router'; 7 | 8 | const Home: NextPage = () => { 9 | const { connected } = useWallet(); 10 | const { setVisible } = useWalletModal(); 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | if (connected) { 15 | router.push('/state') 16 | } 17 | }, [connected, router]) 18 | 19 | function didTapAuthenticate() { 20 | setVisible(true) 21 | } 22 | 23 | return ( 24 |
25 |

Welcome to Wonka

26 |

27 | Wonka JS simplifies the minting process with MetaPlex{"'"}s Candy Machine. Once you have followed the 28 | instructions to upload your NFTs, you can easily use the following commands to build your mint flow:{' '} 29 |

30 |
31 |
    32 |
  • 33 | getCandyMachineState(..) 34 |
  • 35 |
  • 36 | mintCandyMachineToken(..) 37 |
  • 38 |
  • 39 | getCandyMachineMints(..) 40 |
  • 41 |
  • 42 | getMintMetadata(..) 43 |
  • 44 |
45 |
46 |

47 | To get started, first you need to connect your wallet. We are using{' '} 48 | wallet adapter from Solana Labs below, but you can 49 | also write you own. 50 |

51 | 52 |
54 | ); 55 | }; 56 | 57 | export default Home; 58 | -------------------------------------------------------------------------------- /examples/next-js/pages/metadata.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Image from 'next/image'; 3 | import { useRouter } from 'next/router'; 4 | import Button from '../components/Button'; 5 | import { SpinnerCircularFixed } from 'spinners-react'; 6 | import { useConnection, useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'; 7 | import { Wonka, CandyMachineState } from '@triton-labs/wonka'; 8 | import { Provider } from '@project-serum/anchor'; 9 | import { useState, useEffect, useRef } from 'react'; 10 | import { Metadata } from '@metaplex-foundation/mpl-token-metadata'; 11 | import { PublicKey } from '@solana/web3.js'; 12 | 13 | interface Mint { 14 | key: string; 15 | name: string; 16 | imageURL: string; 17 | } 18 | 19 | const Mints: NextPage = () => { 20 | // State: 21 | const [wonka, setWonka] = useState(null); 22 | const [mintMetadata, setMintMetadata] = useState(); 23 | const [mint, setMint] = useState(); 24 | 25 | // Hooks: 26 | const { connection } = useConnection(); 27 | const anchorWallet = useAnchorWallet(); 28 | const router = useRouter(); 29 | 30 | // 1. Create Wonka. 31 | useEffect(() => { 32 | if (anchorWallet && connection) { 33 | const provider = new Provider(connection, anchorWallet, { 34 | preflightCommitment: 'processed', 35 | }); 36 | setWonka(new Wonka(provider, process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!)); 37 | } else { 38 | setWonka(null); 39 | } 40 | }, [anchorWallet, connection]); 41 | 42 | // 2. Fetch mint metadata. 43 | useEffect(() => { 44 | async function fetchMintMetadata() { 45 | if (!wonka) { 46 | return; 47 | } 48 | const mintAddressString = router.query.address; 49 | if (mintAddressString) { 50 | const mintAddress = new PublicKey(mintAddressString); 51 | const metadata = await wonka.getMintMetadata(mintAddress); 52 | const data = await (await fetch(metadata.data.data.uri)).json(); 53 | setMintMetadata(metadata); 54 | setMint({ key: metadata.data.mint, name: data.name, imageURL: data.image }); 55 | } else { 56 | router.push('/mints'); 57 | } 58 | } 59 | fetchMintMetadata(); 60 | }, [wonka, router]); 61 | 62 | function didTapBack() { 63 | router.push('mints'); 64 | } 65 | 66 | return ( 67 |
68 |

Fetching Mint Metdata

69 |

70 | Using getMintMetadata(..) to fetch more information about the mint. 71 |

72 | {mint && mintMetadata ? ( 73 |
74 |
    75 |
  • 76 | Name: {mint.name} 77 |
  • 78 |
  • 79 | Symbol: {mintMetadata.data.data.symbol ?? "undefined"} 80 |
  • 81 |
  • 82 | Collection: {mintMetadata.data.collection ?? "undefined"} 83 |
  • 84 |
  • 85 | Primary Sale Happened: {mintMetadata.data.primarySaleHappened ? 'YES' : 'NO'} 86 |
  • 87 |
  • 88 | Is Mutable: {mintMetadata.data.isMutable ? 'YES' : 'NO'} 89 |
  • 90 |
  • 91 | Number of Creators: {mintMetadata.data.data.creators!.length} 92 |
  • 93 |
  • 94 | Seller Fee Basis Points: {mintMetadata.data.data.sellerFeeBasisPoints} 95 |
  • 96 |
97 |
98 | {`wagmii 99 |
100 |
101 | ) : ( 102 | 103 | )} 104 |
105 |
107 |
108 | ); 109 | }; 110 | 111 | export default Mints; 112 | -------------------------------------------------------------------------------- /examples/next-js/pages/mint.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useRouter } from 'next/router'; 3 | import Button from '../components/Button'; 4 | import { useConnection, useAnchorWallet, useWallet } from '@solana/wallet-adapter-react'; 5 | import { Wonka } from '@triton-labs/wonka'; 6 | import { Provider } from '@project-serum/anchor'; 7 | import { useState, useEffect } from 'react'; 8 | import { PublicKey } from '@solana/web3.js'; 9 | import { toast } from 'react-toastify'; 10 | 11 | function solscanMintLink(address: string, cluster: string): string { 12 | return `https://solscan.io/token/${address}?cluster=${cluster}`; 13 | } 14 | 15 | function solscanTxidLink(txid: string, cluster: string): string { 16 | return `https://solscan.io/tx/${txid}?cluster=${cluster}`; 17 | } 18 | 19 | enum MintState { 20 | READY = 0, 21 | QUEUED, 22 | MINTING, 23 | } 24 | 25 | namespace MintState { 26 | export function toString(mintState: MintState): string { 27 | switch (mintState) { 28 | case MintState.READY: 29 | return 'ready'; 30 | case MintState.QUEUED: 31 | return 'queued'; 32 | case MintState.MINTING: 33 | return 'minting'; 34 | } 35 | return 'unknown'; 36 | } 37 | } 38 | 39 | function toastWithMessage(message: string, error: boolean = false) { 40 | if (error) { 41 | toast.error(message, { 42 | position: 'bottom-center', 43 | pauseOnFocusLoss: false, 44 | }); 45 | } else { 46 | toast(message, { 47 | position: 'bottom-center', 48 | pauseOnFocusLoss: false, 49 | }); 50 | } 51 | } 52 | 53 | const MintPage: NextPage = () => { 54 | // State: 55 | const [wonka, setWonka] = useState(null); 56 | const [mintState, setMintState] = useState(MintState.READY); 57 | const [mintAddress, setMintAddress] = useState(); 58 | const [mintTxid, setMintTxid] = useState(); 59 | 60 | // Hooks: 61 | const { connection } = useConnection(); 62 | const { publicKey } = useWallet(); 63 | const anchorWallet = useAnchorWallet(); 64 | const router = useRouter(); 65 | 66 | // 1. Create Wonka. 67 | useEffect(() => { 68 | if (anchorWallet && connection) { 69 | const provider = new Provider(connection, anchorWallet, { 70 | preflightCommitment: 'processed', 71 | }); 72 | setWonka(new Wonka(provider, process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!)); 73 | } else { 74 | setWonka(null); 75 | } 76 | }, [anchorWallet, connection]); 77 | 78 | // 2. Mint a token. 79 | useEffect(() => { 80 | async function mint() { 81 | if (wonka && mintState === MintState.QUEUED) { 82 | setMintState(MintState.MINTING); 83 | const { mintAddress: address, txid, error } = await wonka.mintCandyMachineToken(publicKey!); 84 | if (error) { 85 | toastWithMessage('Unable to mint, check logs for info.', true); 86 | } else { 87 | setMintAddress(address!); 88 | setMintTxid(txid); 89 | } 90 | setMintState(MintState.READY); 91 | } 92 | } 93 | mint(); 94 | }, [wonka, mintState, publicKey]); 95 | 96 | function didTapMint() { 97 | setMintState(MintState.QUEUED); 98 | } 99 | 100 | function didTapContinue() { 101 | router.push('/mints'); 102 | } 103 | 104 | function didTapBack() { 105 | router.push('/state') 106 | } 107 | 108 | return ( 109 |
110 |

Mint a New Token

111 |

112 | Use mintCandyMachineToken(...) to mint a new token. Once successfully minted, you should see the 113 | token's public address and transaction ID. 114 |

115 |
    116 |
  • 117 | Minter State: {MintState.toString(mintState)} 118 |
  • 119 |
  • 120 | Mint Address:{' '} 121 | 122 | {mintAddress ? ( 123 | {mintAddress.toString()} 124 | ) : ( 125 | 'pending' 126 | )} 127 | 128 |
  • 129 |
  • 130 | Mint Transaction: {mintTxid ? {mintTxid} : 'pending'} 131 |
  • 132 |
133 |
134 |
138 |
139 | ); 140 | }; 141 | 142 | export default MintPage; 143 | -------------------------------------------------------------------------------- /examples/next-js/pages/mints.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Image from 'next/image'; 3 | import { SpinnerCircularFixed } from 'spinners-react'; 4 | import { useRouter } from 'next/router'; 5 | import Button from '../components/Button'; 6 | import { useConnection, useAnchorWallet } from '@solana/wallet-adapter-react'; 7 | import { Wonka } from '@triton-labs/wonka'; 8 | import { Provider } from '@project-serum/anchor'; 9 | import { useState, useEffect, useMemo } from 'react'; 10 | import Link from 'next/link'; 11 | 12 | interface Mint { 13 | key: string; 14 | name: string; 15 | imageURL: string; 16 | } 17 | 18 | const Mints: NextPage = () => { 19 | // State: 20 | const [wonka, setWonka] = useState(null); 21 | const [mints, setMints] = useState(); 22 | const [isFetching, setIsFetching] = useState(false); 23 | 24 | // Hooks: 25 | const { connection } = useConnection(); 26 | const anchorWallet = useAnchorWallet(); 27 | const router = useRouter(); 28 | 29 | // 1. Create Wonka. 30 | useEffect(() => { 31 | if (anchorWallet && connection) { 32 | const provider = new Provider(connection, anchorWallet, { 33 | preflightCommitment: 'processed', 34 | }); 35 | setWonka(new Wonka(provider, process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!)); 36 | } else { 37 | setWonka(null); 38 | } 39 | }, [anchorWallet, connection]); 40 | 41 | // 2. Fetch mints. 42 | useMemo(() => { 43 | async function fetchMints() { 44 | if (wonka && !mints && !isFetching) { 45 | setIsFetching(true); 46 | console.log('Fetching mints...'); 47 | const fetchedMints = await wonka.getCandyMachineMints(); 48 | const fetchedMintsData = await Promise.all( 49 | fetchedMints.map(async (metadata) => { 50 | const data = await fetch(metadata.data.data.uri); 51 | const dataJSON = await data.json(); 52 | return { key: metadata.data.mint, name: dataJSON.name, imageURL: dataJSON.image }; 53 | }), 54 | ); 55 | setMints(fetchedMintsData); 56 | setIsFetching(false); 57 | } 58 | } 59 | fetchMints(); 60 | }, [wonka, mints, isFetching]); 61 | 62 | function didTapBack() { 63 | router.push('/mint'); 64 | } 65 | 66 | function didTapContinue() { 67 | router.push('/metadata'); 68 | } 69 | 70 | return ( 71 |
72 |
73 |

Fetching all Mints

74 |

75 | Loading mints without using an indexer takes some time — give it a sec and we will load all existing minta via{' '} 76 | getCandyMachineMints(..). Click on one of the mints to use getMintMetadata(..) to 77 | fetch mint metadata. 78 |

79 | {mints ? ( 80 |
81 | {mints.map((mint, index) => { 82 | console.log(`rendering mint with index: ${index}`); 83 | return ( 84 | 85 |
86 | {`wagmii 87 |

{mint.name}

88 |
89 | 90 | ); 91 | })} 92 |
93 | ) : ( 94 | 95 | )} 96 |
97 |
98 |
100 |

Tap on one of the mints to go to metadata page.

101 |
102 | ); 103 | }; 104 | 105 | export default Mints; 106 | -------------------------------------------------------------------------------- /examples/next-js/pages/state.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useRouter } from 'next/router'; 3 | import Button from '../components/Button'; 4 | import { useConnection, useAnchorWallet } from '@solana/wallet-adapter-react'; 5 | import { Wonka, CandyMachineState, getMintPrice } from '@triton-labs/wonka'; 6 | import { Provider } from '@project-serum/anchor'; 7 | import { useState, useEffect } from 'react'; 8 | import { SpinnerCircularFixed } from 'spinners-react'; 9 | import { Windex } from '@triton-labs/wonka' 10 | 11 | const CandyMachineState = ({ candyMachineState }: { candyMachineState: CandyMachineState }) => { 12 | return ( 13 |
    14 |
  • 15 | Is Sold Out?: {candyMachineState.isSoldOut ? "Yes" : "No"} 16 |
  • 17 |
  • 18 | Items Available: {candyMachineState.itemsAvailable} 19 |
  • 20 |
  • 21 | Items Redeemed: {candyMachineState.itemsRedeemed} 22 |
  • 23 |
  • 24 | Items Remaining: {candyMachineState.itemsRemaining} 25 |
  • 26 |
  • 27 | Go Live Date: {candyMachineState.goLiveDate.toUTCString()} 28 |
  • 29 |
  • 30 | Price: {getMintPrice(candyMachineState)} 31 |
  • 32 |
33 | ); 34 | }; 35 | 36 | const StatePage: NextPage = () => { 37 | // State: 38 | const [wonka, setWonka] = useState(null); 39 | const [candyMachineState, setCandyMachineState] = useState(); 40 | 41 | // Hooks: 42 | const { connection } = useConnection(); 43 | const anchorWallet = useAnchorWallet(); 44 | const router = useRouter(); 45 | 46 | // Sneak peak at Windex ;) 47 | useEffect(() => { 48 | console.log("foo") 49 | async function fetch() { 50 | const windex = new Windex(process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!) 51 | const collection = await windex.fetchCollection() 52 | console.log(collection) 53 | } 54 | fetch() 55 | }, []) 56 | 57 | // 1. Create Wonka. 58 | useEffect(() => { 59 | if (anchorWallet && connection) { 60 | const provider = new Provider(connection, anchorWallet, { 61 | preflightCommitment: 'processed', 62 | }); 63 | setWonka(new Wonka(provider, process.env.NEXT_PUBLIC_CANDY_MACHINE_ID!)); 64 | } else { 65 | setWonka(null); 66 | } 67 | }, [anchorWallet, connection]); 68 | 69 | // 2. Fetch state. 70 | useEffect(() => { 71 | async function fetchState() { 72 | if (wonka) { 73 | const state = await wonka.getCandyMachineState(); 74 | setCandyMachineState(state); 75 | } 76 | } 77 | fetchState(); 78 | }, [wonka]); 79 | 80 | function didTapContinue() { 81 | router.push('mint'); 82 | } 83 | 84 | return ( 85 |
86 |

Candy Machine State

87 |

88 | Using getCandyMachineState(...) to fetch candy machine state. Useful for knowing how many mints are still available and if the candy machine is already sold out! 89 |

90 | {candyMachineState ? ( 91 | 92 | ) : ( 93 | 94 | )} 95 |
96 |
98 |
99 | ); 100 | }; 101 | 102 | export default StatePage; 103 | -------------------------------------------------------------------------------- /examples/next-js/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/next-js/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wonka-labs/wonka-js/61f54010f3cd67ece2fd036cd0238f5f4b3bab86/examples/next-js/public/favicon.ico -------------------------------------------------------------------------------- /examples/next-js/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next-js/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body{ 6 | background-color: #09080d; 7 | width: 100%; 8 | } 9 | 10 | p, 11 | h1, 12 | ul, 13 | a { 14 | color: white; 15 | @apply text-lg; 16 | @apply py-2 17 | } 18 | 19 | a, button { 20 | text-decoration: underline; 21 | } 22 | 23 | code { 24 | background-color: rgba(249, 249, 249, 0.3); 25 | font-family: monospace; 26 | } -------------------------------------------------------------------------------- /examples/next-js/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /examples/next-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wonka-labs/wonka-js", 3 | "version": "1.0.13", 4 | "description": "Wonka JS is the easiest way to mint and fetch NFTs through JS APIs.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "jest --config jestconfig.json", 9 | "build": "tsc", 10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 11 | "lint": "tslint -p tsconfig.json", 12 | "prepare": "npm run build", 13 | "prepublishOnly": "npm test && npm run lint", 14 | "preversion": "npm run lint", 15 | "version": "npm run format && git add -A src", 16 | "postversion": "git push && git push --tags" 17 | }, 18 | "repository": { 19 | "url": "https://github.com/wonka-labs/wonka-js" 20 | }, 21 | "keywords": [ 22 | "candy", 23 | "machine", 24 | "metaplex", 25 | "nft", 26 | "solana", 27 | "crypto", 28 | "mint" 29 | ], 30 | "files": [ 31 | "lib/**/*" 32 | ], 33 | "author": "@0xZoZoZo, @kunal_modi", 34 | "license": "ISC", 35 | "dependencies": { 36 | "@metaplex-foundation/mpl-token-metadata": "^1.1.0", 37 | "@metaplex/js": "^4.12.0", 38 | "@project-serum/anchor": "^0.24.2", 39 | "@solana/spl-token": "^0.1.8", 40 | "@solana/wallet-adapter-base": "^0.9.2", 41 | "@solana/web3.js": "^1.30.2", 42 | "@testing-library/jest-dom": "^5.11.4", 43 | "@testing-library/react": "^11.1.0", 44 | "@testing-library/user-event": "^12.1.10", 45 | "arweave": "^1.10.18", 46 | "assert": "^2.0.0", 47 | "graphql-request": "^4.0.0", 48 | "loglevel": "^1.8.0", 49 | "next": "^12.0.7", 50 | "react": "^17.0.2", 51 | "react-dom": "^17.0.2", 52 | "react-scripts": "^3.0.1", 53 | "web-vitals": "^1.0.1" 54 | }, 55 | "devDependencies": { 56 | "@types/jest": "^27.4.0", 57 | "@webpack-cli/generators": "^1.1.0", 58 | "husky": "^7.0.4", 59 | "jest": "^27.4.7", 60 | "prettier": "^2.5.1", 61 | "ts-jest": "^27.1.2", 62 | "tslint": "^6.1.3", 63 | "tslint-config-prettier": "^1.18.0", 64 | "typescript": "^4.5.5", 65 | "webpack-cli": "^4.9.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/__tests__/arweave-uploader-tests.ts: -------------------------------------------------------------------------------- 1 | import { _sanitizeB64String } from '../arweave-uploader'; 2 | 3 | test('Should correctly sanitize base64', () => { 4 | expect(_sanitizeB64String("data:image/png;base64, iVBORw0KGgoAAAA")).toBe('iVBORw0KGgoAAAA'); 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/minting-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { _getWarningMesssage } from '../utils/minting-utils'; 2 | 3 | test('Should get correct error message for not enough sol error', () => { 4 | expect(_getWarningMesssage({ 5 | message: 'error SendTransactionError: failed to send transaction: Transaction simulation failed: Error processing Instruction 4: custom program error: 0x1778', 6 | })).toBe('Not enough SOL to pay for this minting'); 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/windex-tests.ts: -------------------------------------------------------------------------------- 1 | import { Windex } from '../windex'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { AssertionError } from 'assert'; 4 | 5 | const testCandyMachineId = new PublicKey('Hkunn4hct84zSPNpyQygThUKn8RUBVf5b4r975NRaHPb'); 6 | const testWalletAddress = new PublicKey('BYeHCJtokQecDkN34ZE4fWgF7U4vDtwjX6bkaiaprQmt'); 7 | 8 | test('should be able to fetch sol name', () => { 9 | return Windex.fetchSolDomainMetadataByAddress(testWalletAddress).then(results => { 10 | expect(results.address).toBe(testWalletAddress.toString()); 11 | expect(results.solName).toBe("kunalm.sol"); 12 | expect(results.twitter).toBe("@kunal_modi"); 13 | }); 14 | }); 15 | 16 | test('should be able to fetch address by sol name', () => { 17 | return Windex.fetchAddressBySolDomain('kunalm.sol').then((results) => { 18 | expect(results!.address).toBe('BYeHCJtokQecDkN34ZE4fWgF7U4vDtwjX6bkaiaprQmt'); 19 | expect(results!.solName).toBe('kunalm.sol'); 20 | expect(results!.twitter).toBe('@kunal_modi'); 21 | }); 22 | }); 23 | 24 | test('should gracefully handle a sol name that does not exist', () => { 25 | return Windex.fetchAddressBySolDomain('null-ice-ice-baby-null.sol').then((results) => { 26 | expect(results).toBeNull(); 27 | }); 28 | }); 29 | 30 | test('should gracefully handle a sol name that has address by not twitter handle', () => { 31 | return Windex.fetchAddressBySolDomain('zorayr.sol').then((results) => { 32 | expect(results).not.toBeNull(); 33 | expect(results!.address).toBe('6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6'); 34 | expect(results!.solName).toBe('zorayr.sol'); 35 | expect(results!.twitter).toBeNull(); 36 | }); 37 | }); 38 | 39 | test('should throw exception in case sol name is invalid', async () => { 40 | await expect(Windex.fetchAddressBySolDomain('zorayr')).rejects.toThrow('Sol domain names should end in sol.'); 41 | }); 42 | 43 | test('should be able to fetch candy machine state', () => { 44 | return Windex.fetchCandyMachineState(testCandyMachineId).then(results => { 45 | expect(results.items_available).toBe(100); 46 | expect(results.price).toBe(1000000000); 47 | expect(results.items_redeemed).toBeDefined(); 48 | }); 49 | }); 50 | 51 | test('should be able to fetch first candy machine NFT', () => { 52 | return Windex.fetchNFTsByCandyMachineID(testCandyMachineId, 2).then(results => { 53 | expect(results[0].address).toBeDefined() 54 | expect(results[0].name).toBeDefined() 55 | expect(results[0].image_url).toBeDefined() 56 | }); 57 | }, 10000); 58 | 59 | test('should be able to fetch NFT by Wallet', () => { 60 | const testWalletId = new PublicKey("6zsuBDfuvtxK5FD9tf8u8LfrYBVnxDWRhj43snmC6Qx6") 61 | return Windex.fetchNFTsByWallet(testWalletId, 2).then(results => { 62 | expect(results[0].address).toBeDefined() 63 | expect(results[0].name).toBeDefined() 64 | expect(results[0].image_url).toBeDefined() 65 | }); 66 | }, 10000); 67 | 68 | test('should be able to fetch NFT by Wallet (mainnet)', () => { 69 | const testWalletId = new PublicKey("BYeHCJtokQecDkN34ZE4fWgF7U4vDtwjX6bkaiaprQmt") 70 | return Windex.fetchNFTsByWallet(testWalletId, 2, Windex.MAINNET_ENDPOINT).then(results => { 71 | expect(results[0].address).toBeDefined() 72 | expect(results[0].name).toBeDefined() 73 | expect(results[0].image_url).toBeDefined() 74 | }); 75 | }, 10000); 76 | 77 | test('should be able to fetch NFT by ID', () => { 78 | const testNFTId = new PublicKey("GsuMovmB1rrYqHeTvoheAr8LFJTLktqrr6U2hZRvKDSj") 79 | return Windex.fetchNFTByMintAddress(testNFTId).then(results => { 80 | expect(results!.address).toBe("GsuMovmB1rrYqHeTvoheAr8LFJTLktqrr6U2hZRvKDSj"); 81 | expect(results!.name).toBe("Wagmify PFP 45"); 82 | expect(results!.image_url).toBe("https://www.arweave.net/p0zxL9xdNDA_lcRCTOiTCISa4hnG19GDQIjulEiIUNs?ext=png"); 83 | }); 84 | }, 10000); 85 | -------------------------------------------------------------------------------- /src/arweave-uploader.ts: -------------------------------------------------------------------------------- 1 | import Arweave from 'arweave'; 2 | import { JWKInterface } from 'arweave/node/lib/wallet'; 3 | import log from 'loglevel'; 4 | 5 | export function _sanitizeB64String(b64string: string) { 6 | const pngHTMLB64StringPrefix = "data:image/png;base64," 7 | if (b64string.startsWith(pngHTMLB64StringPrefix)) { 8 | return b64string.slice(pngHTMLB64StringPrefix.length).trim() 9 | } 10 | return b64string 11 | } 12 | 13 | /** 14 | * Probably worth at one point to extract this out as a separate NPM. 15 | * Specifically the purpose is to provide easy ways to upload specific 16 | * file types to Arweave i.e. PNG, JSON... 17 | */ 18 | export default class ArweaveUploader { 19 | private _arweaveKey: JWKInterface; 20 | private _arweave: Arweave; 21 | 22 | public constructor(arweaveKey: string) { 23 | this._arweaveKey = JSON.parse(arweaveKey) as JWKInterface 24 | this._arweave = Arweave.init({ 25 | host: 'arweave.net', 26 | port: 443, 27 | protocol: 'https' 28 | }); 29 | } 30 | 31 | public async uploadJSON(json: any) { 32 | const buf = Buffer.from(JSON.stringify(json)); 33 | return await this.uploadBuffer(buf, 'application/json', 'json'); 34 | } 35 | 36 | public async uploadBase64PNG(b64string: string) { 37 | const buf = Buffer.from(_sanitizeB64String(b64string), 'base64'); 38 | return await this.uploadBuffer(buf, 'image/png', 'png'); 39 | } 40 | 41 | public async uploadBuffer(buf: ArrayBufferLike, mimeType: string, arweaveExt?: string): Promise { 42 | log.info('uploadBuffer', mimeType, arweaveExt); 43 | const transaction = await this._arweave.createTransaction({ data: buf }, this._arweaveKey); 44 | transaction.addTag('Content-Type', mimeType); 45 | await this._arweave.transactions.sign(transaction, this._arweaveKey); 46 | const response = await this._arweave.transactions.post(transaction); 47 | const status = await this._arweave.transactions.getStatus(transaction.id) 48 | log.info(`Completed transaction ${transaction.id} with status code ${status.status}!`) 49 | const ext = arweaveExt ? `?ext=${encodeURIComponent(arweaveExt)}` : '' 50 | return `https://www.arweave.net/${transaction.id}${ext}` 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Wonka from './wonka'; 2 | import Windex from './windex'; 3 | import { getMintPrice, CandyMachineState } from './utils/state-utils'; 4 | import ArweaveUploader from './arweave-uploader'; 5 | 6 | export default Wonka; 7 | 8 | export { Wonka, Windex, CandyMachineState, ArweaveUploader, getMintPrice }; 9 | -------------------------------------------------------------------------------- /src/program-ids.ts: -------------------------------------------------------------------------------- 1 | import { web3 } from '@project-serum/anchor'; 2 | 3 | /** 4 | * We get these numbers from the CLI scripts source: 5 | * https://github.com/metaplex-foundation/metaplex/blob/master/js/packages/cli/src/helpers/constants.ts 6 | */ 7 | const CANDY_MACHINE_PROGRAM_ID = new web3.PublicKey('cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ'); 8 | const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); 9 | const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new web3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); 10 | 11 | export { CANDY_MACHINE_PROGRAM_ID, TOKEN_METADATA_PROGRAM_ID, SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID }; 12 | -------------------------------------------------------------------------------- /src/utils/connection-utils.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProvider as Provider } from '@project-serum/anchor'; 2 | import { Keypair, Transaction, TransactionInstruction, PublicKey } from '@solana/web3.js'; 3 | 4 | export const sendTransaction = async ( 5 | provider: Provider, 6 | feePayer: PublicKey, 7 | instructions: TransactionInstruction[], 8 | signers: Keypair[], 9 | ) => { 10 | const transaction = new Transaction(); 11 | instructions.forEach((instruction) => transaction.add(instruction)); 12 | transaction.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash; 13 | transaction.feePayer = feePayer; 14 | transaction.partialSign(...signers); 15 | return await provider.sendAndConfirm(transaction, signers, { 16 | skipPreflight: false, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/metadata-utils.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey, TransactionError } from '@solana/web3.js'; 2 | import { Metadata, MetadataProgram, MetadataDataData } from '@metaplex-foundation/mpl-token-metadata'; 3 | import { Wallet, actions } from '@metaplex/js'; 4 | import ArweaveUploader from '../arweave-uploader'; 5 | import { getCandyMachineCreator } from '../utils/pda-utils' 6 | import log from 'loglevel'; 7 | 8 | const MAX_NAME_LENGTH = 32; 9 | const MAX_URI_LENGTH = 200; 10 | const MAX_SYMBOL_LENGTH = 10; 11 | const MAX_CREATOR_LEN = 32 + 1 + 1; 12 | 13 | const getCandyMachineMints = async (candyMachineId: string, connection: Connection): Promise => { 14 | const candyMachineCreatorId = (await getCandyMachineCreator(new PublicKey(candyMachineId)))[0].toString() 15 | const metadataAccounts = await MetadataProgram.getProgramAccounts(connection, { 16 | filters: [ 17 | { 18 | memcmp: { 19 | offset: 20 | 1 + 21 | 32 + 22 | 32 + 23 | 4 + 24 | MAX_NAME_LENGTH + 25 | 4 + 26 | MAX_URI_LENGTH + 27 | 4 + 28 | MAX_SYMBOL_LENGTH + 29 | 2 + 30 | 1 + 31 | 4 + 32 | 0 * MAX_CREATOR_LEN, 33 | bytes: candyMachineCreatorId, 34 | }, 35 | }, 36 | ], 37 | }); 38 | return await Promise.all( 39 | metadataAccounts.map(async (account) => { 40 | const accountInfo = await connection.getAccountInfo(account.pubkey); 41 | const metadata = new Metadata(account.pubkey, accountInfo!); 42 | return metadata; 43 | }), 44 | ); 45 | }; 46 | 47 | const getMintMetadata = async (connection: Connection, mintAddress: PublicKey): Promise => { 48 | const metadataPDA = await Metadata.getPDA(mintAddress); 49 | log.info(`Loading metadata PDA ${metadataPDA.toString()} for token address: ${mintAddress.toString()}.`) 50 | return await Metadata.load(connection, metadataPDA); 51 | } 52 | 53 | const getMintMetadataDataData = async (connection: Connection, mintAddress: PublicKey): Promise => { 54 | const metadata = await getMintMetadata(connection, mintAddress) 55 | return metadata.data.data as MetadataDataData 56 | } 57 | 58 | const updateMintMetadata = async ( 59 | connection: Connection, 60 | arweaveUploader: ArweaveUploader, 61 | wallet: Wallet, 62 | mintKey: PublicKey, 63 | imageContext: any, 64 | jsonUpdate: (metadataDataDataURIJSON: any) => void): Promise<{ txid: string, error?: TransactionError }> => { 65 | const metadataDataData = await getMintMetadataDataData(connection, mintKey) 66 | const metadataDataDataURI = await fetch(metadataDataData.uri) 67 | const metadataDataDataURIJSON = await metadataDataDataURI.json() 68 | jsonUpdate(metadataDataDataURIJSON); 69 | const metadataDataDataJSONArweaveURI = await arweaveUploader.uploadJSON(metadataDataDataURIJSON) 70 | metadataDataData.uri = metadataDataDataJSONArweaveURI 71 | const txid = await actions.updateMetadata({ 72 | connection, 73 | editionMint: new PublicKey(mintKey), 74 | wallet, 75 | newMetadataData: metadataDataData, 76 | }) 77 | log.info(`Starting update metadata transaction with id:${txid}.`) 78 | return new Promise((resolve, reject) => { 79 | connection.onSignatureWithOptions( 80 | txid, 81 | async (notification, _) => { 82 | log.info(`Got notification of type: ${notification.type} from txid: ${txid}.`); 83 | if (notification.type === 'status') { 84 | const { result } = notification; 85 | if (result.err) { 86 | reject({ txid, error: result.err }); 87 | } else { 88 | resolve({ txid }); 89 | } 90 | } 91 | }, 92 | { commitment: 'processed' }, 93 | ); 94 | }); 95 | } 96 | 97 | const updateMintImage = async ( 98 | b64image: string, 99 | connection: Connection, 100 | arweaveUploader: ArweaveUploader, 101 | wallet: Wallet, 102 | mintAddress: PublicKey, 103 | imageContext: any) => { 104 | const uri = await arweaveUploader.uploadBase64PNG(b64image) 105 | log.info(`Uploaded base64 image to arweave, here is the url: ${uri}`) 106 | return await updateMintMetadata(connection, arweaveUploader, wallet, mintAddress, imageContext, json => { 107 | json.image = uri 108 | json.properties.files[0].uri = uri 109 | json.imageContext = imageContext 110 | }) 111 | } 112 | 113 | const updateMintGLB = async ( 114 | glb: ArrayBufferLike, 115 | b64image: string, 116 | connection: Connection, 117 | arweaveUploader: ArweaveUploader, 118 | wallet: Wallet, 119 | mintAddress: PublicKey, 120 | imageContext: any) => { 121 | const animationUri = await arweaveUploader.uploadBuffer(glb, 'model/gltf-binary', 'glb'); 122 | log.info(`Uploaded glb to arweave, here is the url: ${animationUri}`) 123 | const imageUri = await arweaveUploader.uploadBase64PNG(b64image) 124 | log.info(`Uploaded base64 preview image to arweave, here is the url: ${imageUri}`) 125 | return await updateMintMetadata(connection, arweaveUploader, wallet, mintAddress, imageContext, json => { 126 | json.image = imageUri 127 | json.animation_url = animationUri; 128 | json.imageContext = imageContext 129 | const properties = { 130 | files: [ 131 | { 132 | uri: imageUri, 133 | type: "image/png", 134 | }, 135 | { 136 | uri: animationUri, 137 | type: "model/gltf-binary", 138 | }, 139 | ], 140 | category: "vr", 141 | } 142 | if (json.properties?.creators) { 143 | /* tslint:disable:no-string-literal */ 144 | properties['creators'] = json.properties!.creators 145 | } 146 | json.properties = properties 147 | }) 148 | } 149 | 150 | export { 151 | getCandyMachineMints, 152 | getMintMetadata, 153 | updateMintImage, 154 | updateMintGLB, 155 | }; 156 | -------------------------------------------------------------------------------- /src/utils/minting-utils.ts: -------------------------------------------------------------------------------- 1 | import { Program, AnchorProvider as Provider, web3 } from '@project-serum/anchor'; 2 | import { PublicKey, Keypair } from '@solana/web3.js'; 3 | import { Token } from '@solana/spl-token'; 4 | import * as anchor from '@project-serum/anchor'; 5 | import { sendTransaction } from './connection-utils'; 6 | import { MintLayout, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'; 7 | import { Metadata, MasterEdition } from '@metaplex-foundation/mpl-token-metadata'; 8 | import { CANDY_MACHINE_PROGRAM_ID, TOKEN_METADATA_PROGRAM_ID } from '../program-ids'; 9 | import { getCandyMachineCreator, getTokenWallet } from "../utils/pda-utils" 10 | import assert from 'assert'; 11 | import log from 'loglevel'; 12 | 13 | // These are now generated by Metaplex's Rust-to-JS code: 14 | // https://github.com/metaplex-foundation/metaplex-program-library/blob/master/candy-machine/js/src/generated/errors/index.ts 15 | // but that library isn't quite usable yet, so we just hardcode them for now. 16 | const errorMessages = [ 17 | ['0x1770', 'Account does not have correct owner!'], 18 | ['0x1771', 'Account is not initialized!'], 19 | ['0x1772', 'Mint Mismatch!'], 20 | ['0x1773', 'Index greater than length!'], 21 | ['0x1774', 'Numerical overflow error!'], 22 | ['0x1775', 'Can only provide up to 4 creators to candy machine (because candy machine is one)!'], 23 | ['0x1776', 'Uuid must be exactly of 6 length'], 24 | ['0x1777', 'Not enough tokens to pay for this minting'], 25 | ['0x1778', 'Not enough SOL to pay for this minting'], 26 | ['0x1779', 'Token transfer failed'], 27 | ['0x177a', 'Candy machine is empty!'], 28 | ['0x177b', 'Candy machine is not live!'], 29 | ['0x177c', 'Configs that are using hidden uris do not have config lines, they have a single hash representing hashed order'], 30 | ['0x177d', 'Cannot change number of lines unless is a hidden config'], 31 | ['0x177e', 'Derived key invalid'], 32 | ['0x177f', 'Public key mismatch'], 33 | ['0x1780', 'No whitelist token present'], 34 | ['0x1781', 'Token burn failed'], 35 | ['0x1782', 'Missing gateway app when required'], 36 | ['0x1783', 'Missing gateway token when required'], 37 | ['0x1784', 'Invalid gateway token expire time'], 38 | ['0x1785', 'Missing gateway network expire feature when required'], 39 | ['0x1786', 'Unable to find an unused config line near your random number index'], 40 | ['0x1787', 'Invalid string'], 41 | ['0x1788', 'Suspicious transaction detected'], 42 | ['0x1789', 'Cannot Switch to Hidden Settings after items available is greater than 0'], 43 | ['0x178a', 'Incorrect SlotHashes PubKey'], 44 | ['0x178b', 'Incorrect collection NFT authority'], 45 | ['0x178c', 'Collection PDA address is invalid'], 46 | ['0x178d', 'Provided mint account doesn\'t match collection PDA mint'], 47 | ]; 48 | 49 | const _getWarningMesssage = (error: any) => { 50 | if (error.message) { 51 | for (const err of errorMessages) { 52 | if (error.message.indexOf(`: ${err[0]}`) >= 0) { 53 | return err[1]; 54 | } 55 | } 56 | } 57 | return 'Minting failed! Please try again!'; 58 | }; 59 | 60 | const _createAssociatedTokenAccountInstruction = ( 61 | associatedTokenAddress: PublicKey, 62 | payer: PublicKey, 63 | walletAddress: PublicKey, 64 | splTokenMintAddress: PublicKey, 65 | ) => { 66 | const keys = [ 67 | { pubkey: payer, isSigner: true, isWritable: true }, 68 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 69 | { pubkey: walletAddress, isSigner: false, isWritable: false }, 70 | { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, 71 | { 72 | pubkey: web3.SystemProgram.programId, 73 | isSigner: false, 74 | isWritable: false, 75 | }, 76 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 77 | { 78 | pubkey: web3.SYSVAR_RENT_PUBKEY, 79 | isSigner: false, 80 | isWritable: false, 81 | }, 82 | ]; 83 | return new web3.TransactionInstruction({ 84 | keys, 85 | programId: ASSOCIATED_TOKEN_PROGRAM_ID, 86 | data: Buffer.from([]), 87 | }); 88 | }; 89 | 90 | const _mintCandyMachineToken = async ( 91 | provider: Provider, 92 | candyMachineAddress: PublicKey, 93 | recipientWalletAddress: PublicKey, 94 | ) => { 95 | assert(provider !== null, 'Expecting a valid provider!'); 96 | assert(candyMachineAddress !== null, 'Expecting a valid candy machine address!'); 97 | assert(recipientWalletAddress !== null, 'Expecting a valid recipient address!'); 98 | const mint = Keypair.generate(); 99 | const candyMachineProgramIDL = await Program.fetchIdl(CANDY_MACHINE_PROGRAM_ID, provider); 100 | const candyMachineProgram = new Program(candyMachineProgramIDL!, CANDY_MACHINE_PROGRAM_ID, provider); 101 | const userTokenAccountAddress = await getTokenWallet(recipientWalletAddress, mint.publicKey); 102 | const candyMachine: any = await candyMachineProgram.account.candyMachine.fetch(candyMachineAddress); 103 | const signers = [mint]; 104 | const instructions = [ 105 | anchor.web3.SystemProgram.createAccount({ 106 | fromPubkey: recipientWalletAddress, 107 | newAccountPubkey: mint.publicKey, 108 | space: MintLayout.span, 109 | lamports: await candyMachineProgram.provider.connection.getMinimumBalanceForRentExemption(MintLayout.span), 110 | programId: TOKEN_PROGRAM_ID, 111 | }), 112 | Token.createInitMintInstruction( 113 | TOKEN_PROGRAM_ID, 114 | mint.publicKey, 115 | 0, 116 | recipientWalletAddress, 117 | recipientWalletAddress, 118 | ), 119 | _createAssociatedTokenAccountInstruction( 120 | userTokenAccountAddress, 121 | recipientWalletAddress, 122 | recipientWalletAddress, 123 | mint.publicKey, 124 | ), 125 | Token.createMintToInstruction( 126 | TOKEN_PROGRAM_ID, 127 | mint.publicKey, 128 | userTokenAccountAddress, 129 | recipientWalletAddress, 130 | [], 131 | 1, 132 | ), 133 | ]; 134 | const metadataAddress = await Metadata.getPDA(mint.publicKey); 135 | const masterEdition = await MasterEdition.getPDA(mint.publicKey); 136 | const [candyMachineCreator, creatorBump] = await getCandyMachineCreator(candyMachineAddress); 137 | instructions.push( 138 | // @ts-ignore 139 | await candyMachineProgram.instruction.mintNft(creatorBump, { 140 | accounts: { 141 | candyMachine: candyMachineAddress, 142 | candyMachineCreator, 143 | payer: recipientWalletAddress, 144 | wallet: candyMachine.wallet, 145 | mint: mint.publicKey, 146 | metadata: metadataAddress, 147 | masterEdition, 148 | mintAuthority: recipientWalletAddress, 149 | updateAuthority: recipientWalletAddress, 150 | tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, 151 | tokenProgram: TOKEN_PROGRAM_ID, 152 | systemProgram: web3.SystemProgram.programId, 153 | rent: anchor.web3.SYSVAR_RENT_PUBKEY, 154 | clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, 155 | recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, 156 | instructionSysvarAccount: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, 157 | }, 158 | }), 159 | ); 160 | log.info('About to send transaction with all the candy instructions!'); 161 | const txid = await sendTransaction(provider, recipientWalletAddress, instructions, signers); 162 | log.info(`Sent transaction with id: ${txid} for mint: ${mint.publicKey.toString()}.`); 163 | return { 164 | txid, 165 | mint, 166 | }; 167 | }; 168 | 169 | const mintCandyMachineToken = async ( 170 | provider: Provider, 171 | candyMachineAddress: PublicKey, 172 | recipientWalletAddress: PublicKey, 173 | ) : Promise<{txid?: string, mintAddress?: PublicKey, error?: any, errorMessage?: string }> => { 174 | try { 175 | const { txid, mint } = await _mintCandyMachineToken(provider, candyMachineAddress, recipientWalletAddress); 176 | return new Promise((resolve) => { 177 | provider.connection.onSignatureWithOptions( 178 | txid, 179 | async (notification, context) => { 180 | log.info(`Got notification of type: ${notification.type} from txid: ${txid}.`); 181 | if (notification.type === 'status') { 182 | const { result } = notification; 183 | if (!result.err) { 184 | resolve({ txid, mintAddress: mint.publicKey }); 185 | } 186 | } 187 | }, 188 | { commitment: 'processed' }, 189 | ); 190 | }); 191 | } catch (error) { 192 | return { error, errorMessage: _getWarningMesssage(error) }; 193 | } 194 | }; 195 | 196 | export { 197 | mintCandyMachineToken, 198 | _getWarningMesssage 199 | }; 200 | -------------------------------------------------------------------------------- /src/utils/pda-utils.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { CANDY_MACHINE_PROGRAM_ID } from '../program-ids'; 3 | import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'; 4 | 5 | const getCandyMachineCreator = async (candyMachine: PublicKey): Promise<[PublicKey, number]> => { 6 | return await PublicKey.findProgramAddress( 7 | [Buffer.from('candy_machine'), candyMachine.toBuffer()], 8 | CANDY_MACHINE_PROGRAM_ID, 9 | ); 10 | }; 11 | 12 | const getTokenWallet = async (wallet: PublicKey, mint: PublicKey) => { 13 | return ( 14 | await PublicKey.findProgramAddress( 15 | [wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 16 | ASSOCIATED_TOKEN_PROGRAM_ID, 17 | ) 18 | )[0]; 19 | }; 20 | 21 | export {getCandyMachineCreator, getTokenWallet} -------------------------------------------------------------------------------- /src/utils/state-utils.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@project-serum/anchor' 2 | import { PublicKey} from '@solana/web3.js'; 3 | import { CANDY_MACHINE_PROGRAM_ID } from '../program-ids'; 4 | import * as anchor from '@project-serum/anchor'; 5 | import { 6 | LAMPORTS_PER_SOL, 7 | } from '@solana/web3.js'; 8 | 9 | export interface CandyMachineState { 10 | itemsAvailable: number; 11 | itemsRedeemed: number; 12 | itemsRemaining: number; 13 | treasury: anchor.web3.PublicKey; 14 | tokenMint: anchor.web3.PublicKey; 15 | isSoldOut: boolean; 16 | isActive: boolean; 17 | isPresale: boolean; 18 | goLiveDate: Date; 19 | price: number; 20 | gatekeeper: null | { 21 | expireOnUse: boolean; 22 | gatekeeperNetwork: anchor.web3.PublicKey; 23 | }; 24 | endSettings: null | { 25 | number: anchor.BN; 26 | endSettingType: any; 27 | }; 28 | whitelistMintSettings: null | { 29 | mode: any; 30 | mint: anchor.web3.PublicKey; 31 | presale: boolean; 32 | discountPrice: null | number; 33 | }; 34 | hiddenSettings: null | { 35 | name: string; 36 | uri: string; 37 | hash: Uint8Array; 38 | }; 39 | } 40 | 41 | const numberFormater = new Intl.NumberFormat('en-US', { 42 | style: 'decimal', 43 | minimumFractionDigits: 2, 44 | maximumFractionDigits: 2, 45 | }); 46 | 47 | export const formatNumber = { 48 | format: (val?: number) => { 49 | if (!val) { 50 | return '--'; 51 | } 52 | return numberFormater.format(val / LAMPORTS_PER_SOL); 53 | }, 54 | }; 55 | 56 | export function getMintPrice(candyMachineState: CandyMachineState): string { 57 | const price = formatNumber.format( 58 | candyMachineState.isPresale && candyMachineState.whitelistMintSettings?.discountPrice 59 | ? candyMachineState.whitelistMintSettings?.discountPrice! 60 | : candyMachineState.price!, 61 | ); 62 | 63 | return `◎ ${price}`; 64 | }; 65 | 66 | export async function getCandyMachineState(provider: Provider, candyMachineId: PublicKey): Promise { 67 | const idl = await anchor.Program.fetchIdl(CANDY_MACHINE_PROGRAM_ID, provider); 68 | const program = new anchor.Program(idl!, CANDY_MACHINE_PROGRAM_ID, provider); 69 | const state: any = await program.account.candyMachine.fetch(candyMachineId); 70 | const itemsAvailable = state.data.itemsAvailable.toNumber(); 71 | const itemsRedeemed = state.itemsRedeemed.toNumber(); 72 | const itemsRemaining = itemsAvailable - itemsRedeemed; 73 | const presale = 74 | state.data.whitelistMintSettings && 75 | state.data.whitelistMintSettings.presale && 76 | (!state.data.goLiveDate || state.data.goLiveDate.toNumber() > new Date().getTime() / 1000); 77 | const isActive = 78 | (presale || state.data.goLiveDate?.toNumber() < new Date().getTime() / 1000) && 79 | (state.data.endSettings 80 | ? state.data.endSettings.endSettingType.date 81 | ? state.data.endSettings.number.toNumber() > new Date().getTime() / 1000 82 | : itemsRedeemed < state.data.endSettings.number.toNumber() 83 | : true); 84 | const goLiveDate = new Date(state.data.goLiveDate.toNumber() * 1000); 85 | const price = state.data.price.toNumber(); 86 | const whitelistMintSettings = state.data.whitelistMintSettings 87 | if (whitelistMintSettings && whitelistMintSettings.discountPrice) { 88 | whitelistMintSettings.discountPrice = whitelistMintSettings.discountPrice.toNumber() 89 | } 90 | return { 91 | itemsAvailable, 92 | itemsRedeemed, 93 | itemsRemaining, 94 | isSoldOut: itemsRemaining === 0, 95 | isActive, 96 | isPresale: presale, 97 | goLiveDate, 98 | treasury: state.wallet, 99 | tokenMint: state.tokenMint, 100 | gatekeeper: state.data.gatekeeper, 101 | endSettings: state.data.endSettings, 102 | whitelistMintSettings, 103 | hiddenSettings: state.data.hiddenSettings, 104 | price, 105 | }; 106 | } -------------------------------------------------------------------------------- /src/windex.ts: -------------------------------------------------------------------------------- 1 | import { request, gql } from 'graphql-request'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import log from 'loglevel'; 4 | 5 | export interface SolDomainMetadata { 6 | address: string; 7 | solName?: string; 8 | twitter?: string; 9 | } 10 | 11 | export interface CandyMachineState { 12 | id: string; 13 | items_redeemed: number; 14 | items_available: number; 15 | price: number; 16 | go_live_date: number; 17 | } 18 | 19 | export interface CollectionItem { 20 | address: string; 21 | name: string; 22 | symbol: string | null; 23 | description: string | null | undefined; 24 | external_url: string | null | undefined; 25 | image_url: string; 26 | explorer_url: string; 27 | creators: { 28 | address: string; 29 | verified: boolean; 30 | share: number; 31 | }[]; 32 | files: { 33 | uri: string | null; 34 | type: string | null; 35 | }[]; 36 | attributes: { 37 | trait_type: string | null; 38 | value: string | null; 39 | }[]; 40 | } 41 | 42 | type nft = { 43 | id: string; 44 | name: string; 45 | symbol: string | null; 46 | explorer_url: string; 47 | image: { 48 | orig: string; 49 | }; 50 | metaplex_metadata: { 51 | mint: string; 52 | creators: { 53 | address: string; 54 | verified: boolean; 55 | share: number; 56 | }[]; 57 | }; 58 | external_metadata: { 59 | description: string | null; 60 | external_url: string | null; 61 | properties: { 62 | files: 63 | | { 64 | uri: string | null; 65 | type: string | null; 66 | }[] 67 | | null; 68 | } | null; 69 | attributes: 70 | | { 71 | trait_type: string | null; 72 | value: string | null; 73 | }[] 74 | | null; 75 | } | null; 76 | }; 77 | 78 | function nftToCollectionItem(n: nft): CollectionItem { 79 | return { 80 | address: n.metaplex_metadata.mint, 81 | name: n.name, 82 | symbol: n.symbol, 83 | description: n.external_metadata?.description, 84 | external_url: n.external_metadata?.external_url, 85 | image_url: n.image.orig, 86 | explorer_url: n.explorer_url, 87 | creators: n.metaplex_metadata.creators, 88 | files: n.external_metadata?.properties?.files || [], 89 | attributes: n.external_metadata?.attributes || [], 90 | }; 91 | } 92 | 93 | function nftsToCollectionItems(nfts: nft[]): CollectionItem[] { 94 | return nfts.map((n): CollectionItem => { 95 | return nftToCollectionItem(n); 96 | }); 97 | } 98 | 99 | /** 100 | * Wonka Indexer is the fastest indexer in the wild west. 101 | * Currently, we have four data sets you can fetch through windex: 102 | * - Candy Machine State 103 | * - NFTs by Candy Machine 104 | * - NFTs by Wallet Address 105 | * - NFT by Mint Address 106 | * - Sol Domain Name by Address [and vice versa] 107 | */ 108 | export default class Windex { 109 | // Solana Devnet (https://explorer.solana.com/?cluster=devnet) 110 | // To explore queries: https://api.wonkalabs.xyz/v0.1/solana/graphiql?cluster=devnet 111 | static DEVNET_ENDPOINT = 'https://api.wonkalabs.xyz/v0.1/solana/devnet/graphql?src=wonka-js'; 112 | 113 | // Solana Mainnet Beta (https://explorer.solana.com/) 114 | // To explore queries: https://api.wonkalabs.xyz/v0.1/solana/graphiql?cluster=mainnet 115 | static MAINNET_ENDPOINT = 'https://api.wonkalabs.xyz/v0.1/solana/mainnet/graphql?src=wonka-js'; 116 | 117 | public static async fetchCandyMachineState( 118 | candyMachineId: PublicKey, 119 | endpoint: string = Windex.DEVNET_ENDPOINT, 120 | ): Promise { 121 | log.info(`Fetching candy machine state for candy machine with ID: ${candyMachineId.toString()}`); 122 | const fetchCandyMachineStateQuery = gql` 123 | { 124 | candyMachineV2(id: "${candyMachineId.toString()}") { 125 | id 126 | items_redeemed 127 | items_available 128 | price 129 | go_live_date 130 | } 131 | }`; 132 | const results = await request(endpoint, fetchCandyMachineStateQuery); 133 | return results.candyMachineV2 as CandyMachineState; 134 | } 135 | 136 | public static async fetchAddressBySolDomain(solDomain: string): Promise { 137 | log.info(`Fetching address by sol domain: ${solDomain}`); 138 | if (!solDomain.endsWith(".sol")) { throw new Error("Sol domain names should end in sol.") } 139 | // 1. First fetch the public address of the sol domain. 140 | const fetchAddressQuery = gql` 141 | { 142 | addressForSolDomainName(domainName: "${solDomain}") 143 | }`; 144 | const fetchAddressQueryResults = await request(Windex.MAINNET_ENDPOINT, fetchAddressQuery); 145 | // 2. If address is found, try to fetch the twitter handle. Long term, we should combine these queries. 146 | if (fetchAddressQueryResults.addressForSolDomainName) { 147 | const address = fetchAddressQueryResults.addressForSolDomainName; 148 | const fetchTwitterHandleQuery = gql` 149 | { 150 | twitterHandleForAddress(address: "${address}") 151 | }`; 152 | const fetchTwitterHandleQueryResults = await request(Windex.MAINNET_ENDPOINT, fetchTwitterHandleQuery); 153 | return { address, solName: solDomain, twitter: fetchTwitterHandleQueryResults.twitterHandleForAddress }; 154 | } 155 | // 3. If nothing else was found, return null. 156 | return null; 157 | } 158 | 159 | public static async fetchSolDomainMetadataByAddress(address: PublicKey): Promise { 160 | log.info(`Fetching sol domain by address: ${address.toString()}`); 161 | const fetchSolNameQuery = gql` 162 | { 163 | solDomainNameForAddress(address: "${address.toString()}") 164 | twitterHandleForAddress(address: "${address.toString()}") 165 | }`; 166 | const results = await request(Windex.MAINNET_ENDPOINT, fetchSolNameQuery); 167 | return { 168 | address: address.toString(), 169 | solName: results.solDomainNameForAddress, 170 | twitter: results.twitterHandleForAddress, 171 | }; 172 | } 173 | 174 | public static async fetchNFTsByCandyMachineID( 175 | candyMachineID: PublicKey, 176 | first: number = 20, 177 | endpoint: string = Windex.DEVNET_ENDPOINT, 178 | ): Promise { 179 | return await Windex.fetchNFTsByCollectionID(candyMachineID, first, endpoint); 180 | } 181 | 182 | public static async fetchNFTsByCollectionID( 183 | collectionID: PublicKey, 184 | first: number = 20, 185 | endpoint: string = Windex.DEVNET_ENDPOINT, 186 | ): Promise { 187 | log.info(`Fetching NFTs by candy machine with ID: ${collectionID.toString()}`); 188 | const fetchNFTsByCandyMachineQuery = gql` 189 | { 190 | nftsByCollection(collectionId:"${collectionID.toString()}", first:${first}) { 191 | edges { 192 | node { 193 | id 194 | name 195 | symbol 196 | explorer_url 197 | image { 198 | orig 199 | } 200 | metaplex_metadata { 201 | mint 202 | creators { 203 | address 204 | verified 205 | share 206 | } 207 | } 208 | external_metadata { 209 | description 210 | external_url 211 | properties { 212 | files { 213 | uri 214 | type 215 | } 216 | } 217 | attributes { 218 | trait_type 219 | value 220 | } 221 | } 222 | } 223 | } 224 | } 225 | }`; 226 | const results = await request(endpoint, fetchNFTsByCandyMachineQuery); 227 | const nfts = results.nftsByCollection.edges.map((edge) => edge.node) as nft[]; 228 | return nftsToCollectionItems(nfts); 229 | } 230 | 231 | public static async fetchNFTsByWallet( 232 | walletAddress: PublicKey, 233 | first: number = 20, 234 | endpoint: string = Windex.DEVNET_ENDPOINT, 235 | ): Promise { 236 | log.info(`Fetching NFTs by wallet with ID: ${walletAddress.toString()}`); 237 | const fetchNFTsByCandyMachineQuery = gql` 238 | { 239 | nftsByWallet(wallet: "${walletAddress.toString()}", first: ${first}) { 240 | edges { 241 | node { 242 | id 243 | name 244 | symbol 245 | explorer_url 246 | image { 247 | orig 248 | } 249 | metaplex_metadata { 250 | mint 251 | creators { 252 | address 253 | verified 254 | share 255 | } 256 | } 257 | external_metadata { 258 | description 259 | external_url 260 | properties { 261 | files { 262 | uri 263 | type 264 | } 265 | } 266 | attributes { 267 | trait_type 268 | value 269 | } 270 | } 271 | } 272 | } 273 | } 274 | }`; 275 | const results = await request(endpoint, fetchNFTsByCandyMachineQuery); 276 | const nfts = results.nftsByWallet.edges.map((edge) => edge.node) as nft[]; 277 | return nftsToCollectionItems(nfts); 278 | } 279 | 280 | public static async fetchNFTByMintAddress( 281 | mintAddress: PublicKey, 282 | endpoint: string = Windex.DEVNET_ENDPOINT, 283 | ): Promise { 284 | log.info(`Fetching NFTs by mint address with ID: ${mintAddress.toString()}`); 285 | const fetchNFTsByCandyMachineQuery = gql` 286 | { 287 | nftByMintID(mintID: "${mintAddress.toString()}") { 288 | id 289 | name 290 | symbol 291 | explorer_url 292 | image { 293 | orig 294 | } 295 | metaplex_metadata { 296 | mint 297 | creators { 298 | address 299 | verified 300 | share 301 | } 302 | } 303 | external_metadata { 304 | description 305 | external_url 306 | properties { 307 | files { 308 | uri 309 | type 310 | } 311 | } 312 | attributes { 313 | trait_type 314 | value 315 | } 316 | } 317 | } 318 | }`; 319 | const results = await request(endpoint, fetchNFTsByCandyMachineQuery); 320 | return nftToCollectionItem(results.nftByMintID); 321 | } 322 | } 323 | 324 | export { Windex }; 325 | -------------------------------------------------------------------------------- /src/wonka.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProvider as Provider } from '@project-serum/anchor'; 2 | import { PublicKey, Connection } from '@solana/web3.js'; 3 | import { web3 } from '@project-serum/anchor'; 4 | import { getCandyMachineMints, getMintMetadata, updateMintImage, updateMintGLB } from './utils/metadata-utils'; 5 | import { mintCandyMachineToken } from './utils/minting-utils'; 6 | import { Wallet } from '@metaplex/js'; 7 | import ArweaveUploader from './arweave-uploader'; 8 | import log from 'loglevel'; 9 | import { Metadata } from '@metaplex-foundation/mpl-token-metadata'; 10 | import {getCandyMachineState, CandyMachineState} from './utils/state-utils'; 11 | 12 | export default class Wonka { 13 | private _provider: Provider; 14 | private _candyMachineId: PublicKey; 15 | 16 | public constructor(provider: Provider, candyMachineId: string) { 17 | this._provider = provider; 18 | this._candyMachineId = new web3.PublicKey(candyMachineId); 19 | log.info(`Initialized a Wonka with candy machine ID: ${candyMachineId}.`); 20 | } 21 | 22 | public async getCandyMachineMints(): Promise { 23 | return await getCandyMachineMints(this._candyMachineId.toString(), this._provider.connection); 24 | } 25 | 26 | public async mintCandyMachineToken(recipientWalletAddress: PublicKey) { 27 | return await mintCandyMachineToken(this._provider, this._candyMachineId, recipientWalletAddress); 28 | } 29 | 30 | public async getMintMetadata(mintAddress: PublicKey): Promise { 31 | return await getMintMetadata(this._provider.connection, mintAddress); 32 | } 33 | 34 | public static async getMintMetadata(connection: Connection, mintAddress: PublicKey) { 35 | return await getMintMetadata(connection, mintAddress); 36 | } 37 | 38 | public async updateMintImage( 39 | b64image: string, 40 | arweaveUploader: ArweaveUploader, 41 | wallet: Wallet, 42 | mintAddress: PublicKey, 43 | imageContext: any, 44 | ) { 45 | return await updateMintImage( 46 | b64image, 47 | this._provider.connection, 48 | arweaveUploader, 49 | wallet, 50 | mintAddress, 51 | imageContext, 52 | ); 53 | } 54 | 55 | public async updateMintGLB( 56 | glb: ArrayBufferLike, 57 | b64image: string, 58 | arweaveUploader: ArweaveUploader, 59 | wallet: Wallet, 60 | mintAddress: PublicKey, 61 | imageContext: any, 62 | ) { 63 | return await updateMintGLB( 64 | glb, 65 | b64image, 66 | this._provider.connection, 67 | arweaveUploader, 68 | wallet, 69 | mintAddress, 70 | imageContext, 71 | ); 72 | } 73 | 74 | public async getCandyMachineState(): Promise { 75 | return await getCandyMachineState(this._provider, this._candyMachineId) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "sourceMap": true, 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "**/__tests__/*"] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"] 3 | } 4 | --------------------------------------------------------------------------------