├── README.md ├── cli ├── .env_example ├── meta_image.png ├── tsconfig.json ├── helpers │ ├── constants.ts │ ├── upload │ │ ├── ipfs.ts │ │ ├── aws.ts │ │ └── arweave.ts │ ├── various.ts │ ├── schema.ts │ ├── instructions.ts │ ├── transactions.ts │ └── accounts.ts ├── commands │ ├── createHero.ts │ ├── upload.ts │ ├── fetchAll.ts │ ├── updateHero.ts │ └── purchaseHero.ts ├── Readme.md ├── types.ts └── cli-hero.ts ├── src ├── react-app-env.d.ts ├── setupTests.ts ├── App.test.tsx ├── reportWebVitals.ts ├── index.tsx ├── logo.svg ├── App.tsx ├── hero_script.ts └── Home.tsx ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── rust ├── token-metadata │ └── program │ │ ├── Xargo.toml │ │ ├── src │ │ ├── lib.rs │ │ ├── entrypoint.rs │ │ ├── error.rs │ │ └── processor.rs │ │ ├── tests │ │ ├── utils │ │ │ ├── assert.rs │ │ │ ├── mod.rs │ │ │ └── metadata.rs │ │ ├── update_metadata_account.rs │ │ └── create_metadata_account.rs │ │ ├── Cargo.toml │ │ └── README.md ├── Cargo.toml ├── tsconfig.json ├── Anchor.toml ├── package.json └── README.md ├── .env.example └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # HeroNFTMarketPlace -------------------------------------------------------------------------------- /cli/.env_example: -------------------------------------------------------------------------------- 1 | HERO_METADATA_PROGRAM_ID= -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /rust/token-metadata/program/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] -------------------------------------------------------------------------------- /cli/meta_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drujustine0122/HeroNFTMarketPlace/HEAD/cli/meta_image.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drujustine0122/HeroNFTMarketPlace/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drujustine0122/HeroNFTMarketPlace/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drujustine0122/HeroNFTMarketPlace/HEAD/public/logo512.png -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | # "token-vault/program", 4 | "token-metadata/program", 5 | # "token-vault/test", 6 | "token-metadata/test", 7 | ] 8 | exclude = [ 9 | ] 10 | -------------------------------------------------------------------------------- /rust/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_CANDY_MACHINE_CONFIG=__PLACEHOLDER__ 2 | REACT_APP_CANDY_MACHINE_ID=__PLACEHOLDER__ 3 | REACT_APP_TREASURY_ADDRESS=__PLACEHOLDER__ 4 | REACT_APP_CANDY_START_DATE=__PLACEHOLDER__ 5 | 6 | REACT_APP_SOLANA_NETWORK=devnet 7 | REACT_APP_SOLANA_RPC_HOST=https://explorer-api.devnet.solana.com 8 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /rust/Anchor.toml: -------------------------------------------------------------------------------- 1 | [registry] 2 | url = "https://anchor.projectserum.com" 3 | 4 | [provider] 5 | cluster = "devnet" 6 | wallet = "~/.config/solana/id.json" 7 | 8 | [programs.mainnet] 9 | nft_candy_machine = "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ" 10 | 11 | [scripts] 12 | test = "ts-mocha -p ./tsconfig.json -t 1000000 test/*.ts" 13 | 14 | [workspace] 15 | members = ["nft-candy-machine"] 16 | -------------------------------------------------------------------------------- /rust/token-metadata/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Token Metadata program for the Solana blockchain. 2 | 3 | pub mod entrypoint; 4 | pub mod error; 5 | pub mod instruction; 6 | pub mod processor; 7 | pub mod state; 8 | pub mod utils; 9 | // Export current sdk types for downstream users building with a different sdk version6ssXYkgorV8uK2zzPWmwCwX4RLvLfpfJpYcnq1xcfifR 10 | pub use solana_program; 11 | 12 | solana_program::declare_id!("3T32Ema3iTBxhnNT3z36LKbj2jwKBWEsWunmVcmD48Pz"); 13 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./../build", 6 | "declaration": false, 7 | "esModuleInterop": true, 8 | "noImplicitAny": false, 9 | "removeComments": false, 10 | "isolatedModules": false, 11 | "experimentalDecorators": true, 12 | "downlevelIteration": true, 13 | "emitDecoratorMetadata": true, 14 | "noLib": false, 15 | "preserveConstEnums": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "resolveJsonModule": true, 18 | "lib": ["dom", "es2019"] 19 | }, 20 | "exclude": ["node_modules", "typings/browser", "typings/browser.d.ts"], 21 | "atom": { 22 | "rewriteTsconfig": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rust/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metaplex/metaplex-programs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Metaplex Program Tests", 6 | "license": "MIT", 7 | "type": "module", 8 | "author": "Metaplex Contributors", 9 | "devDependencies": { 10 | "@types/mocha": "^9.0.0", 11 | "@project-serum/anchor": "^0.13.2", 12 | "mocha": "^9.0.3", 13 | "ts-mocha": "^8.0.0", 14 | "ts-node": "^10.2.1", 15 | "typescript": "^4.3.5", 16 | "@solana/web3.js": "^1.21.0" 17 | }, 18 | "scripts": { 19 | "idl": "node test/idlToTs", 20 | "test": "env MY_WALLET=$HOME/.config/solana/id.json ts-mocha -p ./tsconfig.json -t 1000000 test/*.ts" 21 | }, 22 | "dependencies": { 23 | "@project-serum/common": "^0.0.1-beta.3", 24 | "quicktype-core": "^6.0.70" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rust/token-metadata/program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint definitions 2 | 3 | #![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] 4 | 5 | use { 6 | crate::{error::MetadataError, processor}, 7 | solana_program::{ 8 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 9 | program_error::PrintProgramError, pubkey::Pubkey, 10 | }, 11 | }; 12 | 13 | entrypoint!(process_instruction); 14 | fn process_instruction<'a>( 15 | program_id: &'a Pubkey, 16 | accounts: &'a [AccountInfo<'a>], 17 | instruction_data: &[u8], 18 | ) -> ProgramResult { 19 | if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) { 20 | // catch the error so we can print it 21 | error.print::(); 22 | return Err(error); 23 | } 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /rust/token-metadata/program/tests/utils/assert.rs: -------------------------------------------------------------------------------- 1 | // #[macro_export] 2 | // macro_rules! assert_transport_error { 3 | // ($error:expr, $matcher:pat) => { 4 | // match $error { 5 | // $matcher => { 6 | // assert!(true) 7 | // } 8 | // _ => assert!(false), 9 | // } 10 | // }; 11 | // } 12 | 13 | // #[macro_export] 14 | // macro_rules! assert_custom_error { 15 | // ($error:expr, $matcher:pat) => { 16 | // match $error { 17 | // TransportError::TransactionError(TransactionError::InstructionError( 18 | // 0, 19 | // InstructionError::Custom(x), 20 | // )) => match FromPrimitive::from_i32(x as i32) { 21 | // Some($matcher) => assert!(true), 22 | // _ => assert!(false), 23 | // }, 24 | // _ => assert!(false), 25 | // }; 26 | // }; 27 | // } 28 | -------------------------------------------------------------------------------- /rust/token-metadata/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metaplex-token-metadata" 3 | version = "0.0.1" 4 | description = "Metaplex Metadata" 5 | authors = ["Metaplex Maintainers "] 6 | repository = "https://github.com/metaplex-foundation/metaplex" 7 | license = "Apache-2.0" 8 | edition = "2018" 9 | exclude = ["js/**"] 10 | 11 | [features] 12 | no-entrypoint = [] 13 | test-bpf = [] 14 | 15 | [dependencies] 16 | num-derive = "0.3" 17 | arrayref = "0.3.6" 18 | num-traits = "0.2" 19 | solana-program = "1.7.11" 20 | # metaplex-token-metadata = "0.0.1" 21 | # metaplex-token-vault = { path = "../../token-vault/program", features = [ "no-entrypoint" ], version="0.0.1" } 22 | spl-token = { version="3.1.1", features = [ "no-entrypoint" ] } 23 | thiserror = "1.0" 24 | borsh = "0.9.1" 25 | 26 | [dev-dependencies] 27 | solana-sdk = "1.7.11" 28 | solana-program-test = "1.7.11" 29 | 30 | [lib] 31 | crate-type = ["cdylib", "lib"] 32 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env 25 | .env.* 26 | !.env.example 27 | node_modules/ 28 | .anchor 29 | **/*.rs.bk 30 | build/ 31 | rust/test/nft-candy-machine.js 32 | dist/ 33 | lib/ 34 | deploy/ 35 | docs/lockup-ui/ 36 | .DS_Store 37 | *~ 38 | .idea 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | *.css 43 | *.css.map 44 | !js/packages/metaplex/src/fonts/fonts.css 45 | !js/packages/metaplex/src/utils/globals.css 46 | js/.eslintcache 47 | target 48 | .env 49 | .vscode 50 | bin 51 | config.json 52 | node_modules 53 | ./package-lock.json 54 | hfuzz_target 55 | hfuzz_workspace 56 | **/*.so 57 | **/.DS_Store 58 | .cache 59 | js/packages/web/.env 60 | traits 61 | traits-configuration.json 62 | -------------------------------------------------------------------------------- /cli/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | export const CANDY_MACHINE = 'candy_machine'; 3 | export const MAX_NAME_LENGTH = 32; 4 | export const MAX_URI_LENGTH = 200; 5 | export const MAX_SYMBOL_LENGTH = 10; 6 | export const MAX_CREATOR_LEN = 32 + 1 + 1; 7 | export const ARWEAVE_PAYMENT_WALLET = new PublicKey( 8 | 'HvwC9QSAzvGXhhVrgPmauVwFWcYZhne3hVot9EbHuFTm', 9 | ); 10 | export const CANDY_MACHINE_PROGRAM_ID = new PublicKey( 11 | 'cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ', 12 | ); 13 | export const TOKEN_METADATA_PROGRAM_ID = new PublicKey( 14 | 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', 15 | ); 16 | export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey( 17 | 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 18 | ); 19 | export const TOKEN_PROGRAM_ID = new PublicKey( 20 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 21 | ); 22 | export const FAIR_LAUNCH_PROGRAM_ID = new PublicKey( 23 | 'faircnAB9k59Y4TXmLabBULeuTLgV7TkGMGNkjnA15j', 24 | ); 25 | export const CONFIG_ARRAY_START = 26 | 32 + // authority 27 | 4 + 28 | 6 + // uuid + u32 len 29 | 4 + 30 | 10 + // u32 len + symbol 31 | 2 + // seller fee basis points 32 | 1 + 33 | 4 + 34 | 5 * 34 + // optional + u32 len + actual vec 35 | 8 + //max supply 36 | 1 + //is mutable 37 | 1 + // retain authority 38 | 4; // max number of lines; 39 | export const CONFIG_LINE_SIZE = 4 + 32 + 4 + 200; 40 | 41 | export const CACHE_PATH = './.cache'; 42 | 43 | export const DEFAULT_TIMEOUT = 15000; 44 | 45 | export const EXTENSION_PNG = '.png'; 46 | export const EXTENSION_JPG = '.jpg'; 47 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /cli/helpers/upload/ipfs.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | import fetch from 'node-fetch'; 3 | import { create, globSource } from 'ipfs-http-client'; 4 | 5 | export interface ipfsCreds { 6 | projectId: string; 7 | secretKey: string; 8 | } 9 | 10 | function sleep(ms: number): Promise { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | } 13 | 14 | export async function ipfsUpload( 15 | ipfsCredentials: ipfsCreds, 16 | image: string, 17 | // manifestBuffer: Buffer, 18 | ) { 19 | const tokenIfps = `${ipfsCredentials.projectId}:${ipfsCredentials.secretKey}`; 20 | // @ts-ignore 21 | const ipfs = create('https://ipfs.infura.io:5001'); 22 | 23 | const uploadToIpfs = async source => { 24 | const { cid } = await ipfs.add(source).catch(); 25 | return cid; 26 | }; 27 | 28 | const mediaHash = await uploadToIpfs(globSource(image, { recursive: true })); 29 | log.debug('mediaHash:', mediaHash); 30 | const mediaUrl = `https://ipfs.io/ipfs/${mediaHash}`; 31 | log.debug('mediaUrl:', mediaUrl); 32 | const authIFPS = Buffer.from(tokenIfps).toString('base64'); 33 | await fetch(`https://ipfs.infura.io:5001/api/v0/pin/add?arg=${mediaHash}`, { 34 | headers: { 35 | Authorization: `Basic ${authIFPS}`, 36 | }, 37 | method: 'POST', 38 | }); 39 | log.info('uploaded image for file:', image); 40 | 41 | await sleep(500); 42 | return mediaUrl; 43 | 44 | // const manifestJson = JSON.parse(manifestBuffer.toString('utf8')); 45 | // manifestJson.image = mediaUrl; 46 | // manifestJson.properties.files = manifestJson.properties.files.map(f => { 47 | // return { ...f, uri: mediaUrl }; 48 | // }); 49 | 50 | // const manifestHash = await uploadToIpfs( 51 | // Buffer.from(JSON.stringify(manifestJson)), 52 | // ); 53 | // await fetch( 54 | // `https://ipfs.infura.io:5001/api/v0/pin/add?arg=${manifestHash}`, 55 | // { 56 | // headers: { 57 | // Authorization: `Basic ${authIFPS}`, 58 | // }, 59 | // method: 'POST', 60 | // }, 61 | // ); 62 | 63 | // await sleep(500); 64 | // const link = `https://ipfs.io/ipfs/${manifestHash}`; 65 | // log.info('uploaded manifest: ', link); 66 | 67 | // return link; 68 | } 69 | -------------------------------------------------------------------------------- /cli/helpers/upload/aws.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | import { basename } from 'path'; 3 | import { createReadStream } from 'fs'; 4 | import { Readable } from 'form-data'; 5 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 6 | 7 | async function uploadFile( 8 | s3Client: S3Client, 9 | awsS3Bucket: string, 10 | filename: string, 11 | contentType: string, 12 | body: string | Readable | ReadableStream | Blob | Uint8Array | Buffer, 13 | ): Promise { 14 | const mediaUploadParams = { 15 | Bucket: awsS3Bucket, 16 | Key: filename, 17 | Body: body, 18 | ACL: 'public-read', 19 | ContentType: contentType, 20 | }; 21 | 22 | try { 23 | await s3Client.send(new PutObjectCommand(mediaUploadParams)); 24 | log.info('uploaded filename:', filename); 25 | } catch (err) { 26 | log.debug('Error', err); 27 | } 28 | 29 | const url = `https://${awsS3Bucket}.s3.amazonaws.com/${filename}`; 30 | log.debug('Location:', url); 31 | return url; 32 | } 33 | 34 | export async function awsUpload( 35 | awsS3Bucket: string, 36 | file: string, 37 | // manifestBuffer: Buffer, 38 | ) { 39 | const REGION = 'us-east-1'; // TODO: Parameterize this. 40 | const s3Client = new S3Client({ region: REGION }); 41 | const filename = `assets/${basename(file)}`; 42 | log.debug('file:', file); 43 | log.debug('filename:', filename); 44 | 45 | const fileStream = createReadStream(file); 46 | const mediaUrl = await uploadFile( 47 | s3Client, 48 | awsS3Bucket, 49 | filename, 50 | 'image/png', 51 | fileStream, 52 | ); 53 | return mediaUrl; 54 | 55 | // Copied from ipfsUpload 56 | // const manifestJson = JSON.parse(manifestBuffer.toString('utf8')); 57 | // manifestJson.image = mediaUrl; 58 | // manifestJson.properties.files = manifestJson.properties.files.map(f => { 59 | // return { ...f, uri: mediaUrl }; 60 | // }); 61 | // const updatedManifestBuffer = Buffer.from(JSON.stringify(manifestJson)); 62 | 63 | // const metadataFilename = filename.replace(/.png$/, '.json'); 64 | // const metadataUrl = await uploadFile( 65 | // s3Client, 66 | // awsS3Bucket, 67 | // metadataFilename, 68 | // 'application/json', 69 | // updatedManifestBuffer, 70 | // ); 71 | 72 | // return metadataUrl; 73 | } 74 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { useMemo } from "react"; 3 | 4 | import Home from "./Home"; 5 | 6 | import * as anchor from "@project-serum/anchor"; 7 | import { clusterApiUrl } from "@solana/web3.js"; 8 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 9 | import { 10 | getPhantomWallet, 11 | getSlopeWallet, 12 | getSolflareWallet, 13 | getSolletWallet, 14 | getSolletExtensionWallet, 15 | } from "@solana/wallet-adapter-wallets"; 16 | 17 | import { 18 | ConnectionProvider, 19 | WalletProvider, 20 | } from "@solana/wallet-adapter-react"; 21 | 22 | import { WalletDialogProvider } from "@solana/wallet-adapter-material-ui"; 23 | import { createTheme, ThemeProvider } from "@material-ui/core"; 24 | 25 | const treasury = new anchor.web3.PublicKey( 26 | process.env.REACT_APP_TREASURY_ADDRESS! 27 | ); 28 | 29 | const config = new anchor.web3.PublicKey( 30 | process.env.REACT_APP_CANDY_MACHINE_CONFIG! 31 | ); 32 | 33 | const candyMachineId = new anchor.web3.PublicKey( 34 | process.env.REACT_APP_CANDY_MACHINE_ID! 35 | ); 36 | 37 | const network = process.env.REACT_APP_SOLANA_NETWORK as WalletAdapterNetwork; 38 | 39 | const rpcHost = process.env.REACT_APP_SOLANA_RPC_HOST!; 40 | const connection = new anchor.web3.Connection(rpcHost); 41 | 42 | const startDateSeed = parseInt(process.env.REACT_APP_CANDY_START_DATE!, 10); 43 | 44 | const txTimeout = 30000; // milliseconds (confirm this works for your project) 45 | 46 | const theme = createTheme({ 47 | palette: { 48 | type: 'dark', 49 | }, 50 | overrides: { 51 | MuiButtonBase: { 52 | root: { 53 | justifyContent: 'flex-start', 54 | }, 55 | }, 56 | MuiButton: { 57 | root: { 58 | textTransform: undefined, 59 | padding: '12px 16px', 60 | }, 61 | startIcon: { 62 | marginRight: 8, 63 | }, 64 | endIcon: { 65 | marginLeft: 8, 66 | }, 67 | }, 68 | }, 69 | }); 70 | 71 | const App = () => { 72 | const endpoint = useMemo(() => clusterApiUrl(network), []); 73 | 74 | const wallets = useMemo( 75 | () => [ 76 | getPhantomWallet(), 77 | getSlopeWallet(), 78 | getSolflareWallet(), 79 | getSolletWallet({ network }), 80 | getSolletExtensionWallet({ network }) 81 | ], 82 | [] 83 | ); 84 | 85 | return ( 86 | 87 | 88 | 89 | 90 | 98 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | export default App; 106 | -------------------------------------------------------------------------------- /cli/commands/createHero.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHeroMetadataInstruction, 3 | } from '../helpers/instructions'; 4 | import { sendTransactionWithRetryWithKeypair } from '../helpers/transactions'; 5 | import { 6 | getHeroDataKey, 7 | } from '../helpers/accounts'; 8 | import * as anchor from '@project-serum/anchor'; 9 | import { 10 | Herodata, 11 | CreateHeroMetadataArgs, 12 | METADATA_SCHEMA, 13 | } from '../helpers/schema'; 14 | import { serialize } from 'borsh'; 15 | import { getProgramAccounts } from './fetchAll'; 16 | import { 17 | Keypair, 18 | Connection, 19 | TransactionInstruction, 20 | PublicKey, 21 | } from '@solana/web3.js'; 22 | import log from 'loglevel'; 23 | 24 | export const createNewHero = async ( 25 | connection: Connection, 26 | heroProgramAddress: string, 27 | walletKeypair: Keypair, 28 | heroData: { 29 | name: string, 30 | uri: string, 31 | price: number, 32 | ownerNftAddress: PublicKey, 33 | } 34 | ): Promise<{ 35 | herodataAccount: PublicKey; 36 | } | void> => { 37 | // Validate heroData 38 | if ( 39 | !heroData.name || 40 | !heroData.uri || 41 | isNaN(heroData.price) || 42 | !heroData.ownerNftAddress 43 | ) { 44 | log.error('Invalid heroData', heroData); 45 | return; 46 | } 47 | 48 | log.info(heroData); 49 | // Create wallet from keypair 50 | const wallet = new anchor.Wallet(walletKeypair); 51 | if (!wallet?.publicKey) return; 52 | 53 | const programId = new PublicKey(heroProgramAddress); 54 | 55 | const fetchData = await getProgramAccounts( 56 | connection, 57 | heroProgramAddress, 58 | {}, 59 | ); 60 | 61 | let newHeroId = fetchData.length + 1; 62 | log.info(`New Hero Id: ${newHeroId}`); 63 | 64 | const instructions: TransactionInstruction[] = []; 65 | const signers: anchor.web3.Keypair[] = [/*mint, */walletKeypair]; 66 | 67 | // Create metadata 68 | const herodataAccount = await getHeroDataKey(newHeroId, programId); 69 | log.info(`Generated hero account: ${herodataAccount}`); 70 | const ownerNftPubkey = new PublicKey(heroData.ownerNftAddress); 71 | const pubkeyArray = new Uint8Array(ownerNftPubkey.toBuffer()); 72 | const data = new Herodata({ 73 | id: newHeroId, 74 | name: heroData.name, 75 | uri: heroData.uri, 76 | lastPrice: 0, 77 | listedPrice: heroData.price, 78 | ownerNftAddress: pubkeyArray, 79 | }); 80 | 81 | log.info(data); 82 | let txnData = Buffer.from( 83 | serialize( 84 | METADATA_SCHEMA, 85 | new CreateHeroMetadataArgs({ data, id: newHeroId }), 86 | ), 87 | ); 88 | 89 | instructions.push( 90 | createHeroMetadataInstruction( 91 | herodataAccount, 92 | wallet.publicKey, 93 | txnData, 94 | programId, 95 | ), 96 | ); 97 | 98 | const res = await sendTransactionWithRetryWithKeypair( 99 | connection, 100 | walletKeypair, 101 | instructions, 102 | signers, 103 | ); 104 | 105 | try { 106 | await connection.confirmTransaction(res.txid, 'max'); 107 | } catch { 108 | // ignore 109 | } 110 | 111 | // Force wait for max confirmations 112 | await connection.getParsedConfirmedTransaction(res.txid, 'confirmed'); 113 | log.info('Hero NFT created', res.txid); 114 | return { herodataAccount }; 115 | }; 116 | -------------------------------------------------------------------------------- /cli/commands/upload.ts: -------------------------------------------------------------------------------- 1 | import { EXTENSION_PNG, EXTENSION_JPG } from '../helpers/constants'; 2 | import path from 'path'; 3 | import { 4 | loadWalletKey, 5 | } from '../helpers/accounts'; 6 | import { 7 | Connection, Keypair 8 | } from '@solana/web3.js'; 9 | import log from 'loglevel'; 10 | import { awsUpload } from '../helpers/upload/aws'; 11 | import { arweaveUpload, arweaveMetaUpload } from '../helpers/upload/arweave'; 12 | import { ipfsCreds, ipfsUpload } from '../helpers/upload/ipfs'; 13 | 14 | export async function upload( 15 | connection: Connection, 16 | imgFile: string, 17 | env: string, 18 | keypair: string, 19 | storage: string, 20 | ipfsCredentials: ipfsCreds, 21 | awsS3Bucket: string, 22 | ): Promise { 23 | let uploadSuccessful = true; 24 | 25 | const seen = {}; 26 | const newFiles = []; 27 | 28 | const f = imgFile; 29 | seen[f.replace(EXTENSION_PNG, '').replace(EXTENSION_JPG, '').split('/').pop()] = true; 30 | newFiles.push(f); 31 | const images = newFiles.filter(val => path.extname(val) === EXTENSION_PNG || path.extname(val) === EXTENSION_JPG); 32 | const SIZE = images.length; 33 | 34 | const walletKeyPair = loadWalletKey(keypair); 35 | 36 | for (let i = 0; i < SIZE; i++) { 37 | const image = images[i]; 38 | const imageName = path.basename(image); 39 | const name = imageName.replace(EXTENSION_PNG, '').replace(EXTENSION_JPG, ''); 40 | 41 | log.debug(`Processing file: ${i}`); 42 | if (i % 50 === 0) { 43 | log.info(`Processing file: ${i}`); 44 | } 45 | 46 | let link; 47 | 48 | if (!link) { 49 | try { 50 | if (storage === 'arweave') { 51 | link = await arweaveUpload( 52 | connection, 53 | walletKeyPair, 54 | env, 55 | image, 56 | name, 57 | ); 58 | } else if (storage === 'ipfs') { 59 | link = await ipfsUpload(ipfsCredentials, image); 60 | } else if (storage === 'aws') { 61 | link = await awsUpload(awsS3Bucket, image); 62 | } 63 | 64 | if (link) { 65 | log.info(`Upload succeed: ${link}`); 66 | } 67 | } catch (er) { 68 | uploadSuccessful = false; 69 | log.error(`Error uploading file ${name}`, er); 70 | } 71 | } 72 | } 73 | 74 | console.log(`Done. Successful = ${uploadSuccessful}.`); 75 | return uploadSuccessful; 76 | } 77 | 78 | export async function uploadMeta( 79 | connection: Connection, 80 | metadata: any, 81 | env: string, 82 | walletKeyPair: Keypair, 83 | ): Promise<{status: boolean, link: string }> { 84 | let uploadSuccessful = true; 85 | 86 | const manifestBuffer = Buffer.from(JSON.stringify(metadata)); 87 | 88 | let link; 89 | try { 90 | link = await arweaveMetaUpload( 91 | walletKeyPair, 92 | connection, 93 | env, 94 | manifestBuffer, 95 | metadata, 96 | 0, 97 | ); 98 | 99 | if (link) { 100 | log.info(`Metadata upload succeed: ${link}`); 101 | } 102 | } catch (er) { 103 | uploadSuccessful = false; 104 | log.error(`Error uploading file ${metadata.name}`, er); 105 | } 106 | console.log(`Done. Successful = ${uploadSuccessful}.`); 107 | return { status: uploadSuccessful, link }; 108 | } 109 | -------------------------------------------------------------------------------- /rust/token-metadata/program/tests/update_metadata_account.rs: -------------------------------------------------------------------------------- 1 | // mod utils; 2 | 3 | // use num_traits::FromPrimitive; 4 | // use solana_program_test::*; 5 | // use solana_sdk::{ 6 | // instruction::InstructionError, 7 | // signature::{Keypair, Signer}, 8 | // transaction::{Transaction, TransactionError}, 9 | // transport::TransportError, 10 | // }; 11 | // use metaplex_token_metadata::error::MetadataError; 12 | // use metaplex_token_metadata::state::Key; 13 | // use metaplex_token_metadata::{id, instruction}; 14 | // use utils::*; 15 | 16 | // #[tokio::test] 17 | // async fn success() { 18 | // let mut context = program_test().start_with_context().await; 19 | // let test_metadata = Metadata::new(); 20 | 21 | // test_metadata 22 | // .create( 23 | // &mut context, 24 | // "Test".to_string(), 25 | // "TST".to_string(), 26 | // "uri".to_string(), 27 | // None, 28 | // 10, 29 | // true, 30 | // ) 31 | // .await 32 | // .unwrap(); 33 | 34 | // test_metadata 35 | // .update( 36 | // &mut context, 37 | // "Cool".to_string(), 38 | // "TST".to_string(), 39 | // "uri".to_string(), 40 | // None, 41 | // 10, 42 | // ) 43 | // .await 44 | // .unwrap(); 45 | 46 | // let metadata = test_metadata.get_data(&mut context).await; 47 | 48 | // assert_eq!(metadata.data.name, "Cool"); 49 | // assert_eq!(metadata.data.symbol, "TST"); 50 | // assert_eq!(metadata.data.uri, "uri"); 51 | // assert_eq!(metadata.data.seller_fee_basis_points, 10); 52 | // assert_eq!(metadata.data.creators, None); 53 | 54 | // assert_eq!(metadata.primary_sale_happened, false); 55 | // assert_eq!(metadata.is_mutable, true); 56 | // assert_eq!(metadata.mint, test_metadata.mint.pubkey()); 57 | // assert_eq!(metadata.update_authority, context.payer.pubkey()); 58 | // assert_eq!(metadata.key, Key::MetadataV1); 59 | // } 60 | 61 | // #[tokio::test] 62 | // async fn fail_invalid_update_authority() { 63 | // let mut context = program_test().start_with_context().await; 64 | // let test_metadata = Metadata::new(); 65 | // let fake_update_authority = Keypair::new(); 66 | 67 | // test_metadata 68 | // .create( 69 | // &mut context, 70 | // "Test".to_string(), 71 | // "TST".to_string(), 72 | // "uri".to_string(), 73 | // None, 74 | // 10, 75 | // true, 76 | // ) 77 | // .await 78 | // .unwrap(); 79 | 80 | // let tx = Transaction::new_signed_with_payer( 81 | // &[instruction::update_metadata_accounts( 82 | // id(), 83 | // test_metadata.pubkey, 84 | // fake_update_authority.pubkey(), 85 | // None, 86 | // None, 87 | // None, 88 | // )], 89 | // Some(&context.payer.pubkey()), 90 | // &[&context.payer, &fake_update_authority], 91 | // context.last_blockhash, 92 | // ); 93 | 94 | // let result = context 95 | // .banks_client 96 | // .process_transaction(tx) 97 | // .await 98 | // .unwrap_err(); 99 | 100 | // assert_custom_error!(result, MetadataError::UpdateAuthorityIncorrect); 101 | // } 102 | -------------------------------------------------------------------------------- /cli/commands/fetchAll.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountInfo, 3 | Connection, 4 | PublicKey, 5 | } from '@solana/web3.js'; 6 | import * as borsh from 'borsh'; 7 | import { AccountAndPubkey, Herodata, Metadata, METADATA_SCHEMA } from '../types'; 8 | import log from 'loglevel'; 9 | 10 | /* 11 | Get accounts by candy machine creator address 12 | Get only verified ones 13 | Get only unverified ones with creator address 14 | Grab n at a time and batch sign and send transaction 15 | 16 | PS: Don't sign candy machine addresses that you do not know about. Signing verifies your participation. 17 | */ 18 | export async function getAllHeros( 19 | connection: Connection, 20 | heroProgramAddress: string, 21 | ) { 22 | const result = await getProgramAccounts( 23 | connection, 24 | heroProgramAddress, 25 | {}, 26 | ); 27 | log.info(`Fetched hero counts: ${result.length}`); 28 | let heroList = []; 29 | for(let hero of result) { 30 | const decoded = await decodeHeroMetadata(hero.account.data); 31 | let metadata = {}; 32 | metadata['id'] = decoded.id; 33 | metadata['lastPrice'] = decoded.lastPrice.toString(); 34 | metadata['listedPrice'] = decoded.listedPrice.toString(); 35 | let name = Buffer.from(decoded.name); 36 | name = name.slice(0, name.indexOf(0)); 37 | let uri = Buffer.from(decoded.uri); 38 | uri = uri.slice(0, uri.indexOf(0)); 39 | metadata['name'] = name.toString(); 40 | metadata['uri'] = uri.toString(); 41 | metadata['ownerNftAddress'] = (new PublicKey(decoded.ownerNftAddress)).toBase58(); 42 | const accountPubkey = hero.pubkey; 43 | heroList.push({ 44 | pubkey: accountPubkey, 45 | data: metadata, 46 | }); 47 | }; 48 | return heroList; 49 | } 50 | 51 | export async function getProgramAccounts( 52 | connection: Connection, 53 | programId: String, 54 | configOrCommitment?: any, 55 | ): Promise> { 56 | const extra: any = {}; 57 | let commitment; 58 | //let encoding; 59 | 60 | if (configOrCommitment) { 61 | if (typeof configOrCommitment === 'string') { 62 | commitment = configOrCommitment; 63 | } else { 64 | commitment = configOrCommitment.commitment; 65 | //encoding = configOrCommitment.encoding; 66 | 67 | if (configOrCommitment.dataSlice) { 68 | extra.dataSlice = configOrCommitment.dataSlice; 69 | } 70 | 71 | if (configOrCommitment.filters) { 72 | extra.filters = configOrCommitment.filters; 73 | } 74 | } 75 | } 76 | 77 | const args = connection._buildArgs([programId], commitment, 'base64', extra); 78 | const unsafeRes = await (connection as any)._rpcRequest( 79 | 'getProgramAccounts', 80 | args, 81 | ); 82 | //console.log(unsafeRes) 83 | const data = ( 84 | unsafeRes.result as Array<{ 85 | account: AccountInfo<[string, string]>; 86 | pubkey: string; 87 | }> 88 | ).map(item => { 89 | return { 90 | account: { 91 | // TODO: possible delay parsing could be added here 92 | data: Buffer.from(item.account.data[0], 'base64'), 93 | executable: item.account.executable, 94 | lamports: item.account.lamports, 95 | // TODO: maybe we can do it in lazy way? or just use string 96 | owner: item.account.owner, 97 | } as AccountInfo, 98 | pubkey: item.pubkey, 99 | }; 100 | }); 101 | 102 | return data; 103 | } 104 | 105 | export async function decodeHeroMetadata(buffer) { 106 | return borsh.deserializeUnchecked(METADATA_SCHEMA, Herodata, buffer); 107 | } 108 | 109 | export async function decodeMetadata(buffer) { 110 | return borsh.deserializeUnchecked(METADATA_SCHEMA, Metadata, buffer); 111 | } -------------------------------------------------------------------------------- /cli/helpers/upload/arweave.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import FormData from 'form-data'; 3 | import fs from 'fs'; 4 | import log from 'loglevel'; 5 | import fetch from 'node-fetch'; 6 | import { ARWEAVE_PAYMENT_WALLET } from '../constants'; 7 | import { sendTransactionWithRetryWithKeypair } from '../transactions'; 8 | 9 | async function upload(data: FormData,/* manifest, */index) { 10 | log.debug(`trying to upload ${index}`);//: ${manifest.name}`); 11 | return await ( 12 | await fetch( 13 | 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile4', 14 | { 15 | method: 'POST', 16 | // @ts-ignore 17 | body: data, 18 | }, 19 | ) 20 | ).json(); 21 | } 22 | 23 | export async function arweaveUpload( 24 | connection, 25 | walletKeyPair, 26 | // anchorProgram, 27 | env, 28 | image, 29 | // manifestBuffer, 30 | // manifest, 31 | name, 32 | ) { 33 | const storageCost = 2300000; // 0.0023 SOL per file (paid to arweave) 34 | 35 | const instructions = [ 36 | anchor.web3.SystemProgram.transfer({ 37 | fromPubkey: walletKeyPair.publicKey, 38 | toPubkey: ARWEAVE_PAYMENT_WALLET, 39 | lamports: storageCost, 40 | }), 41 | ]; 42 | 43 | const tx = await sendTransactionWithRetryWithKeypair( 44 | connection, 45 | walletKeyPair, 46 | instructions, 47 | [], 48 | 'single', 49 | ); 50 | log.debug('transaction for arweave payment:', tx); 51 | 52 | const manifestBuffer = Buffer.from('Anything').toString('base64'); 53 | const data = new FormData(); 54 | data.append('transaction', tx['txid']); 55 | data.append('env', env); 56 | data.append('file[]', fs.createReadStream(image), { 57 | filename: `image.png`, 58 | contentType: 'image/png', 59 | }); 60 | data.append('file[]', manifestBuffer, 'metadata.json'); 61 | 62 | const result = await upload(data,/* manifest, */name); 63 | 64 | const metadataFile = result.messages?.find( 65 | m => m.filename === 'image.png', 66 | ); 67 | if (metadataFile?.transactionId) { 68 | const link = `https://arweave.net/${metadataFile.transactionId}`; 69 | log.debug(`File uploaded: ${link}`); 70 | return link; 71 | } else { 72 | // @todo improve 73 | throw new Error(`No transaction ID for upload: ${name}`); 74 | } 75 | } 76 | 77 | export async function arweaveMetaUpload( 78 | walletKeyPair, 79 | connection, 80 | env, 81 | // image, 82 | manifestBuffer, 83 | manifest, 84 | index, 85 | ) { 86 | const storageCost = 2300000; // 0.0023 SOL per file (paid to arweave) 87 | 88 | const instructions = [ 89 | anchor.web3.SystemProgram.transfer({ 90 | fromPubkey: walletKeyPair.publicKey, 91 | toPubkey: ARWEAVE_PAYMENT_WALLET, 92 | lamports: storageCost, 93 | }), 94 | ]; 95 | 96 | const tx = await sendTransactionWithRetryWithKeypair( 97 | connection, 98 | walletKeyPair, 99 | instructions, 100 | [], 101 | 'single', 102 | ); 103 | log.debug('transaction for arweave payment:', tx); 104 | const image = __dirname + '\\..\\..\\meta_image.png'; 105 | const data = new FormData(); 106 | data.append('transaction', tx['txid']); 107 | data.append('env', env); 108 | data.append('file[]', fs.createReadStream(image), { 109 | filename: `meta_image.png`, 110 | contentType: 'image/png', 111 | }); 112 | data.append('file[]', manifestBuffer, 'metadata.json'); 113 | const result = await upload(data, manifest); 114 | log.info(result); 115 | const metadataFile = result.messages?.find( 116 | m => m.filename === 'manifest.json', 117 | ); 118 | if (metadataFile?.transactionId) { 119 | const link = `https://arweave.net/${metadataFile.transactionId}`; 120 | log.debug(`File uploaded: ${link}`); 121 | return link; 122 | } else { 123 | // @todo improve 124 | throw new Error(`No transaction ID for upload: ${index}`); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cli/Readme.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Some handy commands are provided for testing as well as a starting point for building other apps that interface with this program. 4 | 5 | Before getting started, create a file named `.env` with the following contents: 6 | 7 | ```txt 8 | HERO_METADATA_PROGRAM_ID="the solana network URL" 9 | ``` 10 | 11 | Alternatively, you can set the above as environment variables. 12 | 13 | For devnet, you can use these: 14 | 15 | Provider URL: https://api.devnet.solana.com 16 | Program ID: 17 | 18 | For mainnet, you can use these: 19 | 20 | Provider URL: https://api.mainnet-beta.solana.com 21 | Program ID: 22 | 23 | Then should open terminal and change to current dir. 24 | 25 | ```sh 26 | cd cli 27 | ``` 28 | 29 | ### Expected args 30 | 31 | - `-k --keypair`: the wallet keypair path. `Required` 32 | - `-e --env`: solana cluster env name. `Default`: devnet 33 | - `-l --log-level `: log level (`debug`, `info`) of test script. `Default`: info 34 | 35 | ## Show 36 | 37 | This command returns the hero accounts created to be sold. 38 | 39 | ```sh 40 | ts-node cli-hero.ts show_all 41 | ``` 42 | 43 | ## Create New Hero 44 | 45 | This command create a new hero account. 46 | 47 | ```sh 48 | ts-node cli-hero.ts create_hero 49 | ``` 50 | 51 | ### Some notes 52 | 53 | - This account must be run by a wallet that is: 54 | - the authority defined in the provided admin account in rust program 55 | - the id of new hero auto increased 56 | - the initial last price of new hero is 0 57 | 58 | ### Expected args 59 | 60 | - `-n --name `: new hero name `Required` 61 | - `-u --uri `: new hero image uri `Required` 62 | - `-p --price `: new hero listed price `Required` 63 | - `-o --owner `: new hero owner nft mint address `Required` 64 | 65 | ## Upate Owned Hero Price 66 | 67 | This command update the listed price of owned hero account. 68 | 69 | ```sh 70 | ts-node cli-hero.ts update_hero_price 71 | ``` 72 | 73 | ### Some notes 74 | 75 | - This account must be run by a wallet that is: 76 | - the payer who is the owner of nft token mint which saved in hero data ownerNftAddress 77 | - the id should be in range of all heros count 78 | 79 | ### Expected args 80 | 81 | - `-i --id `: hero id of current payer owned `Required` 82 | - `-p --price `: new listed price hero `Required` 83 | 84 | ## Purchase Hero 85 | 86 | This account executes a hero purchase. Ordinarily, a purchase would happen whenever since the hero is created. To buy an hero, should pay listed price to the owner of hero: owner of ownerNFTaddress in hero data. 87 | 88 | ```sh 89 | ts-node cli-hero.ts buy_hero 90 | ``` 91 | 92 | ### Expected args 93 | 94 | - `-i --id `: hero Id of purchase `Optional` 95 | - `-n --name `: new hero name after purchase for new buyer `Optional` 96 | - `-u --uri `: new hero image after purchase for new buyer `Optional` 97 | - `-p --price `: new hero listed price after purchase for new buyer `Optional` 98 | 99 | If the buyer is not set the new args, then the saved hero data is used for the new buyer. 100 | 101 | ### Other needed file 102 | 103 | The data file is a JSON file that looks something like this: 104 | 105 | ```png 106 | cli/meta_image.png 107 | ``` 108 | 109 | This file is used while mint NFT for purchase Hero. But not presented in anywhere. 110 | 111 | ## Upload Image to Arweave or Ipfs & Aws 112 | 113 | This command used to upload hero image to arweave, ipfs or aws. 114 | 115 | ```sh 116 | ts-node cli-hero.ts upload_image 117 | ``` 118 | 119 | ``: Image file path to upload 120 | 121 | ### Expected args 122 | 123 | - `-s --storage `: Database to use for storage (arweave, ipfs, aws) 124 | `Default`: 'arweave' 125 | 126 | - `--ipfs-infura-project-id `: Infura IPFS project id (required if using IPFS) 127 | `In case` 128 | 129 | - `--ipfs-infura-secret `: Infura IPFS scret key (required if using IPFS) 130 | `In case` 131 | 132 | - `--aws-s3-bucket `: (existing) AWS S3 Bucket name (required if using aws) 133 | `In case` -------------------------------------------------------------------------------- /rust/token-metadata/program/tests/create_metadata_account.rs: -------------------------------------------------------------------------------- 1 | // mod utils; 2 | 3 | // use num_traits::FromPrimitive; 4 | // use solana_program::pubkey::Pubkey; 5 | // use solana_program_test::*; 6 | // use solana_sdk::{ 7 | // instruction::InstructionError, 8 | // signature::{Keypair, Signer}, 9 | // transaction::{Transaction, TransactionError}, 10 | // transport::TransportError, 11 | // }; 12 | // use metaplex_token_metadata::error::MetadataError; 13 | // use metaplex_token_metadata::state::Key; 14 | // use metaplex_token_metadata::{id, instruction}; 15 | // use utils::*; 16 | 17 | // #[tokio::test] 18 | // async fn success() { 19 | // let mut context = program_test().start_with_context().await; 20 | // let test_metadata = Metadata::new(); 21 | 22 | // test_metadata 23 | // .create( 24 | // &mut context, 25 | // "Test".to_string(), 26 | // "TST".to_string(), 27 | // "uri".to_string(), 28 | // None, 29 | // 10, 30 | // false, 31 | // ) 32 | // .await 33 | // .unwrap(); 34 | 35 | // let metadata = test_metadata.get_data(&mut context).await; 36 | 37 | // assert_eq!(metadata.data.name, "Test"); 38 | // assert_eq!(metadata.data.symbol, "TST"); 39 | // assert_eq!(metadata.data.uri, "uri"); 40 | // assert_eq!(metadata.data.seller_fee_basis_points, 10); 41 | // assert_eq!(metadata.data.creators, None); 42 | 43 | // assert_eq!(metadata.primary_sale_happened, false); 44 | // assert_eq!(metadata.is_mutable, false); 45 | // assert_eq!(metadata.mint, test_metadata.mint.pubkey()); 46 | // assert_eq!(metadata.update_authority, context.payer.pubkey()); 47 | // assert_eq!(metadata.key, Key::MetadataV1); 48 | // } 49 | 50 | // #[tokio::test] 51 | // async fn fail_invalid_mint_authority() { 52 | // let mut context = program_test().start_with_context().await; 53 | // let test_metadata = Metadata::new(); 54 | // let fake_mint_authority = Keypair::new(); 55 | // let payer_pubkey = context.payer.pubkey(); 56 | 57 | // create_mint(&mut context, &test_metadata.mint, &payer_pubkey, None) 58 | // .await 59 | // .unwrap(); 60 | // create_token_account( 61 | // &mut context, 62 | // &test_metadata.token, 63 | // &test_metadata.mint.pubkey(), 64 | // &payer_pubkey, 65 | // ) 66 | // .await 67 | // .unwrap(); 68 | // mint_tokens( 69 | // &mut context, 70 | // &test_metadata.mint.pubkey(), 71 | // &test_metadata.token.pubkey(), 72 | // 1, 73 | // &payer_pubkey, 74 | // None, 75 | // ) 76 | // .await 77 | // .unwrap(); 78 | 79 | // let tx = Transaction::new_signed_with_payer( 80 | // &[instruction::create_metadata_accounts( 81 | // id(), 82 | // test_metadata.pubkey.clone(), 83 | // test_metadata.mint.pubkey(), 84 | // fake_mint_authority.pubkey(), 85 | // context.payer.pubkey().clone(), 86 | // context.payer.pubkey().clone(), 87 | // "Test".to_string(), 88 | // "TST".to_string(), 89 | // "uri".to_string(), 90 | // None, 91 | // 10, 92 | // false, 93 | // false, 94 | // )], 95 | // Some(&context.payer.pubkey()), 96 | // &[&context.payer, &fake_mint_authority], 97 | // context.last_blockhash, 98 | // ); 99 | 100 | // let result = context 101 | // .banks_client 102 | // .process_transaction(tx) 103 | // .await 104 | // .unwrap_err(); 105 | 106 | // assert_custom_error!(result, MetadataError::InvalidMintAuthority); 107 | // } 108 | 109 | // #[tokio::test] 110 | // async fn fail_invalid_metadata_pda() { 111 | // let mut context = program_test().start_with_context().await; 112 | // let mut test_metadata = Metadata::new(); 113 | // test_metadata.pubkey = Pubkey::new_unique(); 114 | 115 | // let result = test_metadata 116 | // .create( 117 | // &mut context, 118 | // "Test".to_string(), 119 | // "TST".to_string(), 120 | // "uri".to_string(), 121 | // None, 122 | // 10, 123 | // false, 124 | // ) 125 | // .await 126 | // .unwrap_err(); 127 | 128 | // assert_custom_error!(result, MetadataError::InvalidMetadataKey); 129 | // } 130 | -------------------------------------------------------------------------------- /cli/commands/updateHero.ts: -------------------------------------------------------------------------------- 1 | import { 2 | updateHeroMetadataInstruction, 3 | } from '../helpers/instructions'; 4 | import { sendTransactionWithRetryWithKeypair } from '../helpers/transactions'; 5 | import { 6 | getHeroDataKey, 7 | } from '../helpers/accounts'; 8 | import * as anchor from '@project-serum/anchor'; 9 | import { 10 | Herodata, 11 | UpdateHeroMetadataArgs, 12 | METADATA_SCHEMA, 13 | } from '../helpers/schema'; 14 | import { serialize } from 'borsh'; 15 | import { TOKEN_PROGRAM_ID } from '../helpers/constants'; 16 | import { getProgramAccounts, decodeHeroMetadata } from './fetchAll'; 17 | import { AccountLayout, u64 } from '@solana/spl-token'; 18 | import { 19 | Keypair, 20 | Connection, 21 | TransactionInstruction, 22 | PublicKey, 23 | } from '@solana/web3.js'; 24 | import BN from 'bn.js'; 25 | import log from 'loglevel'; 26 | 27 | export const updateHero = async ( 28 | connection: Connection, 29 | heroProgramAddress: string, 30 | walletKeypair: Keypair, 31 | id: number, 32 | price: number, 33 | ): Promise => { 34 | // Validate heroData 35 | if ( 36 | isNaN(price) 37 | ) { 38 | log.error('Invalid price', price); 39 | return; 40 | } 41 | 42 | log.info(price); 43 | // Create wallet from keypair 44 | const wallet = new anchor.Wallet(walletKeypair); 45 | if (!wallet?.publicKey) return; 46 | 47 | const programId = new PublicKey(heroProgramAddress); 48 | 49 | const instructions: TransactionInstruction[] = []; 50 | const signers: anchor.web3.Keypair[] = [walletKeypair]; 51 | 52 | // Update metadata 53 | let herodataAccount = await getHeroDataKey(id, programId); 54 | log.info(`Generated hero account: ${herodataAccount}`); 55 | 56 | const result = await getProgramAccounts( 57 | connection, 58 | heroProgramAddress, 59 | {}, 60 | ); 61 | const count = result.length; 62 | log.info(`Fetched hero counts: ${count}`); 63 | if (id > count) { 64 | log.error('Invalid id ', count); 65 | return; 66 | } 67 | 68 | let ownerNftAddress: PublicKey; 69 | for(let hero of result) { 70 | const accountPubkey = hero.pubkey; 71 | if (accountPubkey == herodataAccount.toBase58()) { 72 | const decoded: Herodata = await decodeHeroMetadata(hero.account.data); 73 | ownerNftAddress = new PublicKey(decoded.ownerNftAddress); 74 | break; 75 | } 76 | }; 77 | log.info(`Retrived owner nft address: ${ownerNftAddress}`); 78 | 79 | const fetchData = await getProgramAccounts( 80 | connection, 81 | TOKEN_PROGRAM_ID.toBase58(), 82 | { 83 | filters: [ 84 | { 85 | memcmp: { 86 | offset: 0, 87 | bytes: ownerNftAddress.toBase58(), 88 | }, 89 | }, 90 | { 91 | dataSize: 165 92 | }, 93 | ], 94 | }, 95 | ); 96 | let accountPubkey: string; 97 | let accountOwnerPubkey: string; 98 | for(let token of fetchData) { 99 | accountPubkey = token.pubkey; 100 | let accountData = deserializeAccount(token.account.data); 101 | if (accountData.amount == 1) { 102 | accountOwnerPubkey = accountData.owner; 103 | break; 104 | } 105 | }; 106 | log.info(`Token account address: ${accountPubkey}`); 107 | log.info(`Token account owner: ${accountOwnerPubkey}`); 108 | 109 | let txnData = Buffer.from( 110 | serialize( 111 | METADATA_SCHEMA, 112 | new UpdateHeroMetadataArgs({ id, price: new BN(price) }), 113 | ), 114 | ); 115 | 116 | instructions.push( 117 | updateHeroMetadataInstruction( 118 | herodataAccount, 119 | wallet.publicKey, 120 | new PublicKey(accountPubkey), 121 | txnData, 122 | programId, 123 | ), 124 | ); 125 | 126 | const res = await sendTransactionWithRetryWithKeypair( 127 | connection, 128 | walletKeypair, 129 | instructions, 130 | signers, 131 | ); 132 | 133 | try { 134 | await connection.confirmTransaction(res.txid, 'max'); 135 | } catch { 136 | // ignore 137 | } 138 | 139 | // Force wait for max confirmations 140 | await connection.getParsedConfirmedTransaction(res.txid, 'confirmed'); 141 | log.info('Hero NFT created', res.txid); 142 | return ; 143 | }; 144 | 145 | export const deserializeAccount = (data: Buffer) => { 146 | const accountInfo = AccountLayout.decode(data); 147 | accountInfo.mint = new PublicKey(accountInfo.mint); 148 | accountInfo.owner = new PublicKey(accountInfo.owner); 149 | accountInfo.amount = u64.fromBuffer(accountInfo.amount); 150 | 151 | return accountInfo; 152 | }; -------------------------------------------------------------------------------- /rust/token-metadata/program/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // mod assert; 2 | // // mod edition_marker; 3 | // // mod external_price; 4 | // // mod master_edition_v2; 5 | // mod metadata; 6 | // // mod vault; 7 | 8 | // pub use assert::*; 9 | // // pub use edition_marker::EditionMarker; 10 | // // pub use external_price::ExternalPrice; 11 | // // pub use master_edition_v2::MasterEditionV2; 12 | // pub use metadata::Metadata; 13 | // use solana_program_test::*; 14 | // use solana_sdk::{ 15 | // account::Account, program_pack::Pack, pubkey::Pubkey, signature::Signer, 16 | // signer::keypair::Keypair, system_instruction, transaction::Transaction, transport, 17 | // }; 18 | // use spl_token::state::Mint; 19 | // // pub use vault::Vault; 20 | 21 | // pub fn program_test<'a>() -> ProgramTest { 22 | // ProgramTest::new("metaplex_token_metadata", metaplex_token_metadata::id(), None) 23 | // } 24 | 25 | // pub async fn get_account(context: &mut ProgramTestContext, pubkey: &Pubkey) -> Account { 26 | // context 27 | // .banks_client 28 | // .get_account(*pubkey) 29 | // .await 30 | // .expect("account not found") 31 | // .expect("account empty") 32 | // } 33 | 34 | // pub async fn get_mint(context: &mut ProgramTestContext, pubkey: &Pubkey) -> Mint { 35 | // let account = get_account(context, pubkey).await; 36 | // Mint::unpack(&account.data).unwrap() 37 | // } 38 | 39 | // pub async fn mint_tokens( 40 | // context: &mut ProgramTestContext, 41 | // mint: &Pubkey, 42 | // account: &Pubkey, 43 | // amount: u64, 44 | // owner: &Pubkey, 45 | // additional_signer: Option<&Keypair>, 46 | // ) -> transport::Result<()> { 47 | // let mut signing_keypairs = vec![&context.payer]; 48 | // if let Some(signer) = additional_signer { 49 | // signing_keypairs.push(signer); 50 | // } 51 | 52 | // let tx = Transaction::new_signed_with_payer( 53 | // &[ 54 | // spl_token::instruction::mint_to(&spl_token::id(), mint, account, owner, &[], amount) 55 | // .unwrap(), 56 | // ], 57 | // Some(&context.payer.pubkey()), 58 | // &signing_keypairs, 59 | // context.last_blockhash, 60 | // ); 61 | 62 | // context.banks_client.process_transaction(tx).await 63 | // } 64 | 65 | // pub async fn create_token_account( 66 | // context: &mut ProgramTestContext, 67 | // account: &Keypair, 68 | // mint: &Pubkey, 69 | // manager: &Pubkey, 70 | // ) -> transport::Result<()> { 71 | // let rent = context.banks_client.get_rent().await.unwrap(); 72 | 73 | // let tx = Transaction::new_signed_with_payer( 74 | // &[ 75 | // system_instruction::create_account( 76 | // &context.payer.pubkey(), 77 | // &account.pubkey(), 78 | // rent.minimum_balance(spl_token::state::Account::LEN), 79 | // spl_token::state::Account::LEN as u64, 80 | // &spl_token::id(), 81 | // ), 82 | // spl_token::instruction::initialize_account( 83 | // &spl_token::id(), 84 | // &account.pubkey(), 85 | // mint, 86 | // manager, 87 | // ) 88 | // .unwrap(), 89 | // ], 90 | // Some(&context.payer.pubkey()), 91 | // &[&context.payer, &account], 92 | // context.last_blockhash, 93 | // ); 94 | 95 | // context.banks_client.process_transaction(tx).await 96 | // } 97 | 98 | // pub async fn create_mint( 99 | // context: &mut ProgramTestContext, 100 | // mint: &Keypair, 101 | // manager: &Pubkey, 102 | // freeze_authority: Option<&Pubkey>, 103 | // ) -> transport::Result<()> { 104 | // let rent = context.banks_client.get_rent().await.unwrap(); 105 | 106 | // let tx = Transaction::new_signed_with_payer( 107 | // &[ 108 | // system_instruction::create_account( 109 | // &context.payer.pubkey(), 110 | // &mint.pubkey(), 111 | // rent.minimum_balance(spl_token::state::Mint::LEN), 112 | // spl_token::state::Mint::LEN as u64, 113 | // &spl_token::id(), 114 | // ), 115 | // spl_token::instruction::initialize_mint( 116 | // &spl_token::id(), 117 | // &mint.pubkey(), 118 | // &manager, 119 | // freeze_authority, 120 | // 0, 121 | // ) 122 | // .unwrap(), 123 | // ], 124 | // Some(&context.payer.pubkey()), 125 | // &[&context.payer, &mint], 126 | // context.last_blockhash, 127 | // ); 128 | 129 | // context.banks_client.process_transaction(tx).await 130 | // } 131 | -------------------------------------------------------------------------------- /rust/token-metadata/program/tests/utils/metadata.rs: -------------------------------------------------------------------------------- 1 | // use crate::*; 2 | // use solana_program::borsh::try_from_slice_unchecked; 3 | // use solana_program_test::*; 4 | // use solana_sdk::{ 5 | // pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, transaction::Transaction, 6 | // transport, 7 | // }; 8 | // use metaplex_token_metadata::{ 9 | // id, instruction, 10 | // // state::{Creator, Data, PREFIX}, 11 | // }; 12 | 13 | // #[derive(Debug)] 14 | // pub struct Metadata { 15 | // pub mint: Keypair, 16 | // pub pubkey: Pubkey, 17 | // pub token: Keypair, 18 | // } 19 | 20 | // impl Metadata { 21 | // pub fn new() -> Self { 22 | // let mint = Keypair::new(); 23 | // let mint_pubkey = mint.pubkey(); 24 | // let program_id = id(); 25 | 26 | // let metadata_seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()]; 27 | // let (pubkey, _) = Pubkey::find_program_address(metadata_seeds, &id()); 28 | 29 | // Metadata { 30 | // mint, 31 | // pubkey, 32 | // token: Keypair::new(), 33 | // } 34 | // } 35 | 36 | // pub async fn get_data( 37 | // &self, 38 | // context: &mut ProgramTestContext, 39 | // ) -> metaplex_token_metadata::state::Metadata { 40 | // let account = get_account(context, &self.pubkey).await; 41 | // try_from_slice_unchecked(&account.data).unwrap() 42 | // } 43 | 44 | // pub async fn create( 45 | // &self, 46 | // context: &mut ProgramTestContext, 47 | // name: String, 48 | // symbol: String, 49 | // uri: String, 50 | // creators: Option>, 51 | // seller_fee_basis_points: u16, 52 | // is_mutable: bool, 53 | // ) -> transport::Result<()> { 54 | // create_mint(context, &self.mint, &context.payer.pubkey(), None).await?; 55 | // create_token_account( 56 | // context, 57 | // &self.token, 58 | // &self.mint.pubkey(), 59 | // &context.payer.pubkey(), 60 | // ) 61 | // .await?; 62 | // mint_tokens( 63 | // context, 64 | // &self.mint.pubkey(), 65 | // &self.token.pubkey(), 66 | // 1, 67 | // &context.payer.pubkey(), 68 | // None, 69 | // ) 70 | // .await?; 71 | 72 | // let tx = Transaction::new_signed_with_payer( 73 | // &[instruction::create_metadata_accounts( 74 | // id(), 75 | // self.pubkey.clone(), 76 | // self.mint.pubkey(), 77 | // context.payer.pubkey().clone(), 78 | // context.payer.pubkey().clone(), 79 | // context.payer.pubkey().clone(), 80 | // name, 81 | // symbol, 82 | // uri, 83 | // creators, 84 | // seller_fee_basis_points, 85 | // false, 86 | // is_mutable, 87 | // )], 88 | // Some(&context.payer.pubkey()), 89 | // &[&context.payer], 90 | // context.last_blockhash, 91 | // ); 92 | 93 | // Ok(context.banks_client.process_transaction(tx).await?) 94 | // } 95 | 96 | // // pub async fn update_primary_sale_happened_via_token( 97 | // // &self, 98 | // // context: &mut ProgramTestContext, 99 | // // ) -> transport::Result<()> { 100 | // // let tx = Transaction::new_signed_with_payer( 101 | // // &[instruction::update_primary_sale_happened_via_token( 102 | // // id(), 103 | // // self.pubkey, 104 | // // context.payer.pubkey(), 105 | // // self.token.pubkey(), 106 | // // )], 107 | // // Some(&context.payer.pubkey()), 108 | // // &[&context.payer], 109 | // // context.last_blockhash, 110 | // // ); 111 | 112 | // // Ok(context.banks_client.process_transaction(tx).await?) 113 | // // } 114 | 115 | // pub async fn update( 116 | // &self, 117 | // context: &mut ProgramTestContext, 118 | // name: String, 119 | // symbol: String, 120 | // uri: String, 121 | // creators: Option>, 122 | // seller_fee_basis_points: u16, 123 | // ) -> transport::Result<()> { 124 | // let tx = Transaction::new_signed_with_payer( 125 | // &[instruction::update_metadata_accounts( 126 | // id(), 127 | // self.pubkey, 128 | // context.payer.pubkey().clone(), 129 | // None, 130 | // Some(Data { 131 | // name, 132 | // symbol, 133 | // uri, 134 | // creators, 135 | // seller_fee_basis_points, 136 | // }), 137 | // None, 138 | // )], 139 | // Some(&context.payer.pubkey()), 140 | // &[&context.payer], 141 | // context.last_blockhash, 142 | // ); 143 | 144 | // Ok(context.banks_client.process_transaction(tx).await?) 145 | // } 146 | // } 147 | -------------------------------------------------------------------------------- /cli/helpers/various.ts: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL, AccountInfo } from '@solana/web3.js'; 2 | import fs from 'fs'; 3 | import weighted from 'weighted'; 4 | import path from 'path'; 5 | 6 | const { readFile } = fs.promises; 7 | 8 | export async function readJsonFile(fileName: string) { 9 | const file = await readFile(fileName, 'utf-8'); 10 | return JSON.parse(file); 11 | } 12 | 13 | export const generateRandomSet = breakdown => { 14 | const tmp = {}; 15 | Object.keys(breakdown).forEach(attr => { 16 | const randomSelection = weighted.select(breakdown[attr]); 17 | tmp[attr] = randomSelection; 18 | }); 19 | 20 | return tmp; 21 | }; 22 | 23 | export const getUnixTs = () => { 24 | return new Date().getTime() / 1000; 25 | }; 26 | 27 | export function sleep(ms: number): Promise { 28 | return new Promise(resolve => setTimeout(resolve, ms)); 29 | } 30 | 31 | export function fromUTF8Array(data: number[]) { 32 | // array of bytes 33 | let str = '', 34 | i; 35 | 36 | for (i = 0; i < data.length; i++) { 37 | const value = data[i]; 38 | 39 | if (value < 0x80) { 40 | str += String.fromCharCode(value); 41 | } else if (value > 0xbf && value < 0xe0) { 42 | str += String.fromCharCode(((value & 0x1f) << 6) | (data[i + 1] & 0x3f)); 43 | i += 1; 44 | } else if (value > 0xdf && value < 0xf0) { 45 | str += String.fromCharCode( 46 | ((value & 0x0f) << 12) | 47 | ((data[i + 1] & 0x3f) << 6) | 48 | (data[i + 2] & 0x3f), 49 | ); 50 | i += 2; 51 | } else { 52 | // surrogate pair 53 | const charCode = 54 | (((value & 0x07) << 18) | 55 | ((data[i + 1] & 0x3f) << 12) | 56 | ((data[i + 2] & 0x3f) << 6) | 57 | (data[i + 3] & 0x3f)) - 58 | 0x010000; 59 | 60 | str += String.fromCharCode( 61 | (charCode >> 10) | 0xd800, 62 | (charCode & 0x03ff) | 0xdc00, 63 | ); 64 | i += 3; 65 | } 66 | } 67 | 68 | return str; 69 | } 70 | 71 | export function parsePrice(price: string, mantissa: number = LAMPORTS_PER_SOL) { 72 | return Math.ceil(parseFloat(price) * mantissa); 73 | } 74 | 75 | export function parseDate(date) { 76 | if (date === 'now') { 77 | return Date.now() / 1000; 78 | } 79 | return Date.parse(date) / 1000; 80 | } 81 | 82 | export const getMultipleAccounts = async ( 83 | connection: any, 84 | keys: string[], 85 | commitment: string, 86 | ) => { 87 | const result = await Promise.all( 88 | chunks(keys, 99).map(chunk => 89 | getMultipleAccountsCore(connection, chunk, commitment), 90 | ), 91 | ); 92 | 93 | const array = result 94 | .map( 95 | a => 96 | //@ts-ignore 97 | a.array.map(acc => { 98 | if (!acc) { 99 | return undefined; 100 | } 101 | 102 | const { data, ...rest } = acc; 103 | const obj = { 104 | ...rest, 105 | data: Buffer.from(data[0], 'base64'), 106 | } as AccountInfo; 107 | return obj; 108 | }) as AccountInfo[], 109 | ) 110 | //@ts-ignore 111 | .flat(); 112 | return { keys, array }; 113 | }; 114 | 115 | export function chunks(array, size) { 116 | return Array.apply(0, new Array(Math.ceil(array.length / size))).map( 117 | (_, index) => array.slice(index * size, (index + 1) * size), 118 | ); 119 | } 120 | 121 | export function generateRandoms( 122 | numberOfAttrs: number = 1, 123 | total: number = 100, 124 | ) { 125 | const numbers = []; 126 | const loose_percentage = total / numberOfAttrs; 127 | 128 | for (let i = 0; i < numberOfAttrs; i++) { 129 | const random = Math.floor(Math.random() * loose_percentage) + 1; 130 | numbers.push(random); 131 | } 132 | 133 | const sum = numbers.reduce((prev, cur) => { 134 | return prev + cur; 135 | }, 0); 136 | 137 | numbers.push(total - sum); 138 | return numbers; 139 | } 140 | 141 | export const getMetadata = ( 142 | name: string = '', 143 | symbol: string = '', 144 | index: number = 0, 145 | creators, 146 | description: string = '', 147 | seller_fee_basis_points: number = 500, 148 | attrs, 149 | collection, 150 | ) => { 151 | const attributes = []; 152 | for (const prop in attrs) { 153 | attributes.push({ 154 | trait_type: prop, 155 | value: path.parse(attrs[prop]).name, 156 | }); 157 | } 158 | return { 159 | name: `${name}${index + 1}`, 160 | symbol, 161 | image: `${index}.png`, 162 | properties: { 163 | files: [ 164 | { 165 | uri: `${index}.png`, 166 | type: 'image/png', 167 | }, 168 | ], 169 | category: 'image', 170 | creators, 171 | }, 172 | description, 173 | seller_fee_basis_points, 174 | attributes, 175 | collection, 176 | }; 177 | }; 178 | 179 | const getMultipleAccountsCore = async ( 180 | connection: any, 181 | keys: string[], 182 | commitment: string, 183 | ) => { 184 | const args = connection._buildArgs([keys], commitment, 'base64'); 185 | 186 | const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args); 187 | if (unsafeRes.error) { 188 | throw new Error( 189 | 'failed to get info about account ' + unsafeRes.error.message, 190 | ); 191 | } 192 | 193 | if (unsafeRes.result.value) { 194 | const array = unsafeRes.result.value as AccountInfo[]; 195 | return { keys, array }; 196 | } 197 | 198 | // TODO: fix 199 | throw new Error(); 200 | }; 201 | -------------------------------------------------------------------------------- /rust/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hero Token Program 3 | --- 4 | 5 | ## Background 6 | 7 | Solana's programming model and the definitions of the Solana terms used in this 8 | document are available at: 9 | 10 | - https://docs.solana.com/apps 11 | - https://docs.solana.com/terminology 12 | 13 | 14 | ## Interface 15 | 16 | The on-chain Token Metadata program is written in Rust and available on crates.io as 17 | [metaplex-token-metadata](https://crates.io/crates/metaplex-token-metadata) and 18 | [docs.rs](https://docs.rs/metaplex-token-metadata). 19 | 20 | The crate provides four instructions, `create_metadata_accounts()`, `update_metadata_account()`, `create_master_edition()`, `show(),` to easily create instructions for the program. 21 | 22 | ## Operational overview 23 | 24 | This is a very simple program designed to allow metadata tagging to a given mint, with an update authority 25 | that can change that metadata going forward. Optionally, owners of the metadata can choose to tag this metadata 26 | as a master edition and then use this master edition to label child mints as "limited editions" of this master 27 | edition going forward. The owners of the metadata do not need to be involved in every step of the process, 28 | as any holder of a master edition mint token can have their mint labeled as a limited edition without 29 | the involvement or signature of the owner, this allows for the sale and distribution of master edition prints. 30 | 31 | ## Operational flow for Master Editions 32 | 33 | It would be useful before a dive into architecture to illustrate the flow for a master edition 34 | as a story because it makes it easier to understand. 35 | 36 | 1. User creates a new Metadata for their mint with `create_metadata_accounts()` which makes new `Metadata` 37 | 2. User wishes their mint to be a master edition and ensures that there 38 | is only required supply of one in the mint. 39 | 3. User requests the program to designate `create_master_edition()` on their metadata, 40 | which creates new `MasterEdition` which for this example we will say has an unlimited supply. As 41 | part of the arguments to the function the user is required to make a new mint called the Printing mint over 42 | which they have minting authority that they tell the contract about and that the contract stores on the 43 | `MasterEdition`. 44 | 4. User mints a token from the Printing mint and gives it to their friend. 45 | 5. Their friend creates a new mint with supply 1 and calls `mint_new_edition_from_master_edition_via_token()`, 46 | which creates for them new `Metadata` and `Edition` records signifying this mint as an Edition child of 47 | the master edition original. 48 | 49 | There is a slight variation on this theme if `create_master_edition()` is given a max_supply: minting authority 50 | is locked within the program for the Printing mint and all minting takes place immediately in 51 | `create_master_edition()` to a designated account the user provides and owns - 52 | the user then uses this fixed pool as the source of their authorization tokens going forward to prevent new 53 | supply from being generated in an unauthorized manner. 54 | 55 | ### Permissioning and Architecture 56 | 57 | There are three different major structs in the app: Metadata, MasterEditions, and Editions. A Metadata can 58 | have zero or one MasterEdition, OR can have zero or one Edition, but CANNOT have both a MasterEdition AND 59 | an Edition associated with it. This is to say a Metadata is EITHER a master edition 60 | or a edition(child record) of another master edition. 61 | 62 | Only the minting authority on a mint can create metadata accounts. A Metadata account holds the name, symbol, 63 | and uri of the mint, as well as the mint id. To ensure the uniqueness of 64 | a mint's metadata, the address of a Metadata account is a program derived address composed of seeds: 65 | 66 | ```rust 67 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref()] 68 | ``` 69 | 70 | A master edition is an extension account of this PDA, being simply: 71 | 72 | ```rust 73 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "edition".as_bytes()] 74 | ``` 75 | 76 | Any limited edition minted from this has the same address, but is of a different struct type. The reason 77 | these two different structs(Edition and MasterEdition) share the same address is to ensure that there can 78 | be no Metadata that has both, which would make no sense in the current architecture. 79 | 80 | ### create_metadata_account 81 | 82 | This action creates the `Metadata` account. 83 | 84 | ``` 85 | cd rust/token-metadata/test 86 | ../../target/debug/metaplex-token-metadata-test-client create_metadata_accounts --keypair ~/.config/solana/id.json --name "Test Hero NFT #2" --id 2 --last_price 10 --price 12 --uri https://placeimg.com/200/200/2 --owner "Owner NFT address" 87 | ``` 88 | 89 | ### update_metadata_account 90 | 91 | (Update authority must be signer) 92 | 93 | This call can be called at any time by the update authority to update the URI on any metadata or 94 | update authority on metadata, and later other fields. 95 | 96 | ### create_master_edition 97 | 98 | (Update authority must be signer) 99 | 100 | This can only be called once, and only if the supply on the mint is one. It will create a `MasterEdition` record. 101 | Now other Mints can become Editions of this Metadata if they have the proper authorization token. 102 | 103 | ### show 104 | 105 | Fetch all hero data from program. 106 | 107 | ``` 108 | ../../target/debug/metaplex-token-metadata-test-client show --keypair ~/.config/solana/id.json 109 | ``` 110 | 111 | ### Further extensions 112 | 113 | This program is designed to be extended with further account buckets. 114 | 115 | If say, we wanted to add metadata for youtube metadata, we could create a new struct called Youtube 116 | and seed it with the seed 117 | 118 | ```rust 119 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "youtube".as_bytes()] 120 | ``` 121 | 122 | And then only those interested in that metadata need search for it, and its uniqueness is ensured. It can also 123 | have it's own update action that follows a similar pattern to the original update action. 124 | -------------------------------------------------------------------------------- /cli/helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import { BinaryReader, BinaryWriter } from 'borsh'; 2 | import base58 from 'bs58'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | type StringPublicKey = string; 5 | 6 | import BN from 'bn.js'; 7 | 8 | export class Creator { 9 | address: StringPublicKey; 10 | verified: number; 11 | share: number; 12 | 13 | constructor(args: { 14 | address: StringPublicKey; 15 | verified: number; 16 | share: number; 17 | }) { 18 | this.address = args.address; 19 | this.verified = args.verified; 20 | this.share = args.share; 21 | } 22 | } 23 | 24 | export class Data { 25 | name: string; 26 | symbol: string; 27 | uri: string; 28 | sellerFeeBasisPoints: number; 29 | creators: Creator[] | null; 30 | constructor(args: { 31 | name: string; 32 | symbol: string; 33 | uri: string; 34 | sellerFeeBasisPoints: number; 35 | creators: Creator[] | null; 36 | }) { 37 | this.name = args.name; 38 | this.symbol = args.symbol; 39 | this.uri = args.uri; 40 | this.sellerFeeBasisPoints = args.sellerFeeBasisPoints; 41 | this.creators = args.creators; 42 | } 43 | } 44 | 45 | export class CreateMetadataArgs { 46 | instruction: number = 0; 47 | data: Data; 48 | isMutable: boolean; 49 | 50 | constructor(args: { data: Data; isMutable: boolean }) { 51 | this.data = args.data; 52 | this.isMutable = args.isMutable; 53 | } 54 | } 55 | 56 | export class CreateMasterEditionArgs { 57 | instruction: number = 10; 58 | maxSupply: BN | null; 59 | constructor(args: { maxSupply: BN | null }) { 60 | this.maxSupply = args.maxSupply; 61 | } 62 | } 63 | 64 | export class Herodata { 65 | id: number; 66 | name: string; 67 | uri: string; 68 | lastPrice: number; 69 | listedPrice: number; 70 | ownerNftAddress: Uint8Array; 71 | constructor(args: { 72 | id: number; 73 | name: string; 74 | uri: string; 75 | lastPrice: number; 76 | listedPrice: number; 77 | ownerNftAddress: Uint8Array; 78 | }) { 79 | this.id = args.id; 80 | this.name = args.name; 81 | this.uri = args.uri; 82 | this.lastPrice = args.lastPrice; 83 | this.listedPrice = args.listedPrice; 84 | this.ownerNftAddress = args.ownerNftAddress; 85 | } 86 | } 87 | 88 | export class CreateHeroMetadataArgs { 89 | ins_no: number; 90 | data: Herodata; 91 | id: number; 92 | constructor(args: { data: Herodata, id: number }) { 93 | this.ins_no = 0; 94 | this.data = args.data; 95 | this.id = args.id; 96 | } 97 | } 98 | 99 | export class UpdateHeroMetadataArgs { 100 | id: number; 101 | price: BN; 102 | ins_no: number; 103 | constructor(args: { id: number, price: BN }) { 104 | this.ins_no = 1; 105 | this.id = args.id; 106 | this.price = args.price; 107 | } 108 | } 109 | 110 | export class PurchaseHeroArgs { 111 | id: number; 112 | new_price: BN | null; 113 | new_name: string | null; 114 | new_uri: string | null; 115 | ins_no: number; 116 | constructor(args: { id: number, price: BN | null, name: string | null, uri: string | null }) { 117 | this.ins_no = 2; 118 | this.id = args.id; 119 | this.new_name = args.name; 120 | this.new_uri = args.uri; 121 | this.new_price = args.price; 122 | } 123 | } 124 | 125 | export const METADATA_SCHEMA = new Map([ 126 | [ 127 | CreateHeroMetadataArgs, 128 | { 129 | kind: 'struct', 130 | fields: [ 131 | ['ins_no', 'u8'], 132 | ['data', Herodata], 133 | ['id', 'u8'], 134 | ], 135 | }, 136 | ], 137 | [ 138 | UpdateHeroMetadataArgs, 139 | { 140 | kind: 'struct', 141 | fields: [ 142 | ['ins_no', 'u8'], 143 | ['id', 'u8'], 144 | ['price', 'u64'], 145 | ], 146 | }, 147 | ], 148 | [ 149 | PurchaseHeroArgs, 150 | { 151 | kind: 'struct', 152 | fields: [ 153 | ['ins_no', 'u8'], 154 | ['id', 'u8'], 155 | ['new_name', { kind: 'option', type: 'string' }], 156 | ['new_uri', { kind: 'option', type: 'string' }], 157 | ['new_price', { kind: 'option', type: 'u64' }], 158 | ], 159 | }, 160 | ], 161 | [ 162 | Herodata, 163 | { 164 | kind: 'struct', 165 | fields: [ 166 | ['id', 'u8'], 167 | ['name', 'string'], 168 | ['uri', 'string'], 169 | ['lastPrice', 'u64'], 170 | ['listedPrice', 'u64'], 171 | ['ownerNftAddress', [32]], 172 | ], 173 | }, 174 | ], 175 | [ 176 | CreateMetadataArgs, 177 | { 178 | kind: 'struct', 179 | fields: [ 180 | ['instruction', 'u8'], 181 | ['data', Data], 182 | ['isMutable', 'u8'], // bool 183 | ], 184 | }, 185 | ], 186 | [ 187 | CreateMasterEditionArgs, 188 | { 189 | kind: 'struct', 190 | fields: [ 191 | ['instruction', 'u8'], 192 | ['maxSupply', { kind: 'option', type: 'u64' }], 193 | ], 194 | }, 195 | ], 196 | [ 197 | Data, 198 | { 199 | kind: 'struct', 200 | fields: [ 201 | ['name', 'string'], 202 | ['symbol', 'string'], 203 | ['uri', 'string'], 204 | ['sellerFeeBasisPoints', 'u16'], 205 | ['creators', { kind: 'option', type: [Creator] }], 206 | ], 207 | }, 208 | ], 209 | [ 210 | Creator, 211 | { 212 | kind: 'struct', 213 | fields: [ 214 | ['address', 'pubkeyAsString'], 215 | ['verified', 'u8'], 216 | ['share', 'u8'], 217 | ], 218 | }, 219 | ], 220 | ]); 221 | 222 | export const extendBorsh = () => { 223 | (BinaryReader.prototype as any).readPubkey = function () { 224 | const reader = this as unknown as BinaryReader; 225 | const array = reader.readFixedArray(32); 226 | return new PublicKey(array); 227 | }; 228 | 229 | (BinaryWriter.prototype as any).writePubkey = function (value: PublicKey) { 230 | const writer = this as unknown as BinaryWriter; 231 | writer.writeFixedArray(value.toBuffer()); 232 | }; 233 | 234 | (BinaryReader.prototype as any).readPubkeyAsString = function () { 235 | const reader = this as unknown as BinaryReader; 236 | const array = reader.readFixedArray(32); 237 | return base58.encode(array) as StringPublicKey; 238 | }; 239 | 240 | (BinaryWriter.prototype as any).writePubkeyAsString = function ( 241 | value: StringPublicKey, 242 | ) { 243 | const writer = this as unknown as BinaryWriter; 244 | writer.writeFixedArray(base58.decode(value)); 245 | }; 246 | }; 247 | 248 | extendBorsh(); 249 | -------------------------------------------------------------------------------- /rust/token-metadata/program/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Token Metadata Program 3 | --- 4 | 5 | ## Background 6 | 7 | Solana's programming model and the definitions of the Solana terms used in this 8 | document are available at: 9 | 10 | - https://docs.solana.com/apps 11 | - https://docs.solana.com/terminology 12 | 13 | ## Source 14 | 15 | The Token Metadata Program's source is available on 16 | [github](https://github.com/metaplex-foundation/metaplex) 17 | 18 | There is also an example Rust client located at 19 | [github](https://github.com/metaplex-foundation/metaplex/tree/master/rust/token-metadata/test/src/main.rs) 20 | that can be perused for learning and run if desired with `cargo run --bin metaplex-token-metadata-test-client`. It allows testing out a variety of scenarios. 21 | 22 | ## Interface 23 | 24 | The on-chain Token Metadata program is written in Rust and available on crates.io as 25 | [metaplex-token-metadata](https://crates.io/crates/metaplex-token-metadata) and 26 | [docs.rs](https://docs.rs/metaplex-token-metadata). 27 | 28 | The crate provides four instructions, `create_metadata_accounts()`, `update_metadata_account()`, `create_master_edition()`, `mint_new_edition_from_master_edition_via_token(),` to easily create instructions for the program. 29 | 30 | ## Operational overview 31 | 32 | This is a very simple program designed to allow metadata tagging to a given mint, with an update authority 33 | that can change that metadata going forward. Optionally, owners of the metadata can choose to tag this metadata 34 | as a master edition and then use this master edition to label child mints as "limited editions" of this master 35 | edition going forward. The owners of the metadata do not need to be involved in every step of the process, 36 | as any holder of a master edition mint token can have their mint labeled as a limited edition without 37 | the involvement or signature of the owner, this allows for the sale and distribution of master edition prints. 38 | 39 | ## Operational flow for Master Editions 40 | 41 | It would be useful before a dive into architecture to illustrate the flow for a master edition 42 | as a story because it makes it easier to understand. 43 | 44 | 1. User creates a new Metadata for their mint with `create_metadata_accounts()` which makes new `Metadata` 45 | 2. User wishes their mint to be a master edition and ensures that there 46 | is only required supply of one in the mint. 47 | 3. User requests the program to designate `create_master_edition()` on their metadata, 48 | which creates new `MasterEdition` which for this example we will say has an unlimited supply. As 49 | part of the arguments to the function the user is required to make a new mint called the Printing mint over 50 | which they have minting authority that they tell the contract about and that the contract stores on the 51 | `MasterEdition`. 52 | 4. User mints a token from the Printing mint and gives it to their friend. 53 | 5. Their friend creates a new mint with supply 1 and calls `mint_new_edition_from_master_edition_via_token()`, 54 | which creates for them new `Metadata` and `Edition` records signifying this mint as an Edition child of 55 | the master edition original. 56 | 57 | There is a slight variation on this theme if `create_master_edition()` is given a max_supply: minting authority 58 | is locked within the program for the Printing mint and all minting takes place immediately in 59 | `create_master_edition()` to a designated account the user provides and owns - 60 | the user then uses this fixed pool as the source of their authorization tokens going forward to prevent new 61 | supply from being generated in an unauthorized manner. 62 | 63 | ### Permissioning and Architecture 64 | 65 | There are three different major structs in the app: Metadata, MasterEditions, and Editions. A Metadata can 66 | have zero or one MasterEdition, OR can have zero or one Edition, but CANNOT have both a MasterEdition AND 67 | an Edition associated with it. This is to say a Metadata is EITHER a master edition 68 | or a edition(child record) of another master edition. 69 | 70 | Only the minting authority on a mint can create metadata accounts. A Metadata account holds the name, symbol, 71 | and uri of the mint, as well as the mint id. To ensure the uniqueness of 72 | a mint's metadata, the address of a Metadata account is a program derived address composed of seeds: 73 | 74 | ```rust 75 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref()] 76 | ``` 77 | 78 | A master edition is an extension account of this PDA, being simply: 79 | 80 | ```rust 81 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "edition".as_bytes()] 82 | ``` 83 | 84 | Any limited edition minted from this has the same address, but is of a different struct type. The reason 85 | these two different structs(Edition and MasterEdition) share the same address is to ensure that there can 86 | be no Metadata that has both, which would make no sense in the current architecture. 87 | 88 | ### create_metadata_account 89 | 90 | (Mint authority must be signer) 91 | 92 | This action creates the `Metadata` account. 93 | 94 | ### update_metadata_account 95 | 96 | (Update authority must be signer) 97 | 98 | This call can be called at any time by the update authority to update the URI on any metadata or 99 | update authority on metadata, and later other fields. 100 | 101 | ### create_master_edition 102 | 103 | (Update authority must be signer) 104 | 105 | This can only be called once, and only if the supply on the mint is one. It will create a `MasterEdition` record. 106 | Now other Mints can become Editions of this Metadata if they have the proper authorization token. 107 | 108 | ### mint_new_edition_from_master_edition_via_token 109 | 110 | (Mint authority of new mint must be signer) 111 | 112 | If one possesses a token from the Printing mint of the master edition and a brand new mint with no `Metadata`, and 113 | that mint has only a supply of one, this mint can be turned into an `Edition` of this parent `Master Edition` by 114 | calling this endpoint. This endpoint both creates the `Edition` and `Metadata` records and burns the token. 115 | 116 | ### Further extensions 117 | 118 | This program is designed to be extended with further account buckets. 119 | 120 | If say, we wanted to add metadata for youtube metadata, we could create a new struct called Youtube 121 | and seed it with the seed 122 | 123 | ```rust 124 | ["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "youtube".as_bytes()] 125 | ``` 126 | 127 | And then only those interested in that metadata need search for it, and its uniqueness is ensured. It can also 128 | have it's own update action that follows a similar pattern to the original update action. 129 | -------------------------------------------------------------------------------- /cli/types.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@project-serum/anchor'; 2 | import { PublicKey, AccountInfo } from '@solana/web3.js'; 3 | 4 | export class Creator { 5 | address: PublicKey; 6 | verified: boolean; 7 | share: number; 8 | 9 | constructor(args: { address: PublicKey; verified: boolean; share: number }) { 10 | this.address = args.address; 11 | this.verified = args.verified; 12 | this.share = args.share; 13 | } 14 | } 15 | 16 | export interface Config { 17 | authority: PublicKey; 18 | data: ConfigData; 19 | } 20 | 21 | export class ConfigData { 22 | name: string; 23 | symbol: string; 24 | uri: string; 25 | sellerFeeBasisPoints: number; 26 | creators: Creator[] | null; 27 | maxNumberOfLines: BN | number; 28 | isMutable: boolean; 29 | maxSupply: BN; 30 | retainAuthority: boolean; 31 | 32 | constructor(args: { 33 | name: string; 34 | symbol: string; 35 | uri: string; 36 | sellerFeeBasisPoints: number; 37 | creators: Creator[] | null; 38 | maxNumberOfLines: BN; 39 | isMutable: boolean; 40 | maxSupply: BN; 41 | retainAuthority: boolean; 42 | }) { 43 | this.name = args.name; 44 | this.symbol = args.symbol; 45 | this.uri = args.uri; 46 | this.sellerFeeBasisPoints = args.sellerFeeBasisPoints; 47 | this.creators = args.creators; 48 | this.maxNumberOfLines = args.maxNumberOfLines; 49 | this.isMutable = args.isMutable; 50 | this.maxSupply = args.maxSupply; 51 | this.retainAuthority = args.retainAuthority; 52 | } 53 | } 54 | 55 | export type AccountAndPubkey = { 56 | pubkey: string; 57 | account: AccountInfo; 58 | }; 59 | 60 | export enum MetadataKey { 61 | Uninitialized = 0, 62 | MetadataV1 = 4, 63 | EditionV1 = 1, 64 | MasterEditionV1 = 2, 65 | MasterEditionV2 = 6, 66 | EditionMarker = 7, 67 | } 68 | 69 | export class MasterEditionV1 { 70 | key: MetadataKey; 71 | supply: BN; 72 | maxSupply?: BN; 73 | printingMint: PublicKey; 74 | oneTimePrintingAuthorizationMint: PublicKey; 75 | constructor(args: { 76 | key: MetadataKey; 77 | supply: BN; 78 | maxSupply?: BN; 79 | printingMint: PublicKey; 80 | oneTimePrintingAuthorizationMint: PublicKey; 81 | }) { 82 | this.key = MetadataKey.MasterEditionV1; 83 | this.supply = args.supply; 84 | this.maxSupply = args.maxSupply; 85 | this.printingMint = args.printingMint; 86 | this.oneTimePrintingAuthorizationMint = 87 | args.oneTimePrintingAuthorizationMint; 88 | } 89 | } 90 | 91 | export class MasterEditionV2 { 92 | key: MetadataKey; 93 | supply: BN; 94 | maxSupply?: BN; 95 | constructor(args: { key: MetadataKey; supply: BN; maxSupply?: BN }) { 96 | this.key = MetadataKey.MasterEditionV2; 97 | this.supply = args.supply; 98 | this.maxSupply = args.maxSupply; 99 | } 100 | } 101 | 102 | export class EditionMarker { 103 | key: MetadataKey; 104 | ledger: number[]; 105 | constructor(args: { key: MetadataKey; ledger: number[] }) { 106 | this.key = MetadataKey.EditionMarker; 107 | this.ledger = args.ledger; 108 | } 109 | } 110 | 111 | export class Edition { 112 | key: MetadataKey; 113 | parent: PublicKey; 114 | edition: BN; 115 | constructor(args: { key: MetadataKey; parent: PublicKey; edition: BN }) { 116 | this.key = MetadataKey.EditionV1; 117 | this.parent = args.parent; 118 | this.edition = args.edition; 119 | } 120 | } 121 | 122 | export class Data { 123 | name: string; 124 | symbol: string; 125 | uri: string; 126 | sellerFeeBasisPoints: number; 127 | creators: Creator[] | null; 128 | constructor(args: { 129 | name: string; 130 | symbol: string; 131 | uri: string; 132 | sellerFeeBasisPoints: number; 133 | creators: Creator[] | null; 134 | }) { 135 | this.name = args.name; 136 | this.symbol = args.symbol; 137 | this.uri = args.uri; 138 | this.sellerFeeBasisPoints = args.sellerFeeBasisPoints; 139 | this.creators = args.creators; 140 | } 141 | } 142 | 143 | export class Metadata { 144 | key: MetadataKey; 145 | updateAuthority: PublicKey; 146 | mint: PublicKey; 147 | data: Data; 148 | primarySaleHappened: boolean; 149 | isMutable: boolean; 150 | masterEdition?: PublicKey; 151 | edition?: PublicKey; 152 | constructor(args: { 153 | updateAuthority: PublicKey; 154 | mint: PublicKey; 155 | data: Data; 156 | primarySaleHappened: boolean; 157 | isMutable: boolean; 158 | masterEdition?: PublicKey; 159 | }) { 160 | this.key = MetadataKey.MetadataV1; 161 | this.updateAuthority = args.updateAuthority; 162 | this.mint = args.mint; 163 | this.data = args.data; 164 | this.primarySaleHappened = args.primarySaleHappened; 165 | this.isMutable = args.isMutable; 166 | } 167 | } 168 | 169 | export class Herodata { 170 | id: number; 171 | name: string; 172 | uri: string; 173 | lastPrice: number; 174 | listedPrice: number; 175 | ownerNftAddress: PublicKey; 176 | constructor(args: { 177 | id: number; 178 | name: string; 179 | uri: string; 180 | lastPrice: number; 181 | listedPrice: number; 182 | ownerNftAddress: PublicKey; 183 | }) { 184 | this.id = args.id; 185 | this.name = args.name; 186 | this.uri = args.uri; 187 | this.lastPrice = args.lastPrice; 188 | this.listedPrice = args.listedPrice; 189 | this.ownerNftAddress = args.ownerNftAddress; 190 | } 191 | } 192 | 193 | export const METADATA_SCHEMA = new Map([ 194 | [ 195 | Herodata, 196 | { 197 | kind: 'struct', 198 | fields: [ 199 | ['id', 'u8'], 200 | ['name', 'string'], 201 | ['uri', 'string'], 202 | ['lastPrice', 'u64'], 203 | ['listedPrice', 'u64'], 204 | ['ownerNftAddress', [32]], 205 | ], 206 | }, 207 | ], 208 | [ 209 | MasterEditionV1, 210 | { 211 | kind: 'struct', 212 | fields: [ 213 | ['key', 'u8'], 214 | ['supply', 'u64'], 215 | ['maxSupply', { kind: 'option', type: 'u64' }], 216 | ['printingMint', 'pubkey'], 217 | ['oneTimePrintingAuthorizationMint', [32]], 218 | ], 219 | }, 220 | ], 221 | [ 222 | MasterEditionV2, 223 | { 224 | kind: 'struct', 225 | fields: [ 226 | ['key', 'u8'], 227 | ['supply', 'u64'], 228 | ['maxSupply', { kind: 'option', type: 'u64' }], 229 | ], 230 | }, 231 | ], 232 | [ 233 | Edition, 234 | { 235 | kind: 'struct', 236 | fields: [ 237 | ['key', 'u8'], 238 | ['parent', [32]], 239 | ['edition', 'u64'], 240 | ], 241 | }, 242 | ], 243 | [ 244 | Data, 245 | { 246 | kind: 'struct', 247 | fields: [ 248 | ['name', 'string'], 249 | ['symbol', 'string'], 250 | ['uri', 'string'], 251 | ['sellerFeeBasisPoints', 'u16'], 252 | ['creators', { kind: 'option', type: [Creator] }], 253 | ], 254 | }, 255 | ], 256 | [ 257 | Creator, 258 | { 259 | kind: 'struct', 260 | fields: [ 261 | ['address', [32]], 262 | ['verified', 'u8'], 263 | ['share', 'u8'], 264 | ], 265 | }, 266 | ], 267 | [ 268 | Metadata, 269 | { 270 | kind: 'struct', 271 | fields: [ 272 | ['key', 'u8'], 273 | ['updateAuthority', [32]], 274 | ['mint', [32]], 275 | ['data', Data], 276 | ['primarySaleHappened', 'u8'], 277 | ['isMutable', 'u8'], 278 | ], 279 | }, 280 | ], 281 | [ 282 | EditionMarker, 283 | { 284 | kind: 'struct', 285 | fields: [ 286 | ['key', 'u8'], 287 | ['ledger', [31]], 288 | ], 289 | }, 290 | ], 291 | ]); 292 | -------------------------------------------------------------------------------- /cli/helpers/instructions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | SystemProgram, 4 | SYSVAR_RENT_PUBKEY, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | import { 8 | CANDY_MACHINE_PROGRAM_ID, 9 | CONFIG_ARRAY_START, 10 | CONFIG_LINE_SIZE, 11 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 12 | TOKEN_PROGRAM_ID, 13 | TOKEN_METADATA_PROGRAM_ID, 14 | } from './constants'; 15 | import * as anchor from '@project-serum/anchor'; 16 | 17 | export function createHeroMetadataInstruction( 18 | metadataAccount: PublicKey, 19 | payer: PublicKey, 20 | txnData: Buffer, 21 | programId: PublicKey, 22 | ) { 23 | const keys = [ 24 | { 25 | pubkey: metadataAccount, 26 | isSigner: false, 27 | isWritable: true, 28 | }, 29 | { 30 | pubkey: payer, 31 | isSigner: true, 32 | isWritable: true, 33 | }, 34 | { 35 | pubkey: SystemProgram.programId, 36 | isSigner: false, 37 | isWritable: false, 38 | }, 39 | { 40 | pubkey: SYSVAR_RENT_PUBKEY, 41 | isSigner: false, 42 | isWritable: false, 43 | }, 44 | ]; 45 | return new TransactionInstruction({ 46 | programId, 47 | keys, 48 | data: txnData, 49 | }); 50 | } 51 | 52 | export function updateHeroMetadataInstruction( 53 | metadataAccount: PublicKey, 54 | payer: PublicKey, 55 | tokenAccount: PublicKey, 56 | txnData: Buffer, 57 | programId: PublicKey, 58 | ) { 59 | const keys = [ 60 | { 61 | pubkey: metadataAccount, 62 | isSigner: false, 63 | isWritable: true, 64 | }, 65 | { 66 | pubkey: payer, 67 | isSigner: true, 68 | isWritable: true, 69 | }, 70 | { 71 | pubkey: tokenAccount, 72 | isSigner: false, 73 | isWritable: false, 74 | }, 75 | ]; 76 | return new TransactionInstruction({ 77 | programId, 78 | keys, 79 | data: txnData, 80 | }); 81 | } 82 | 83 | export function purchaseHeroInstruction( 84 | metadataAccount: PublicKey, 85 | payer: PublicKey, 86 | tokenOwnerAddress: PublicKey, 87 | tokenAccount: PublicKey, 88 | newMintAccount: PublicKey, 89 | txnData: Buffer, 90 | programId: PublicKey, 91 | ) { 92 | const keys = [ 93 | { 94 | pubkey: metadataAccount, 95 | isSigner: false, 96 | isWritable: true, 97 | }, 98 | { 99 | pubkey: payer, 100 | isSigner: true, 101 | isWritable: true, 102 | }, 103 | { 104 | pubkey: tokenOwnerAddress, 105 | isSigner: false, 106 | isWritable: true, 107 | }, 108 | { 109 | pubkey: tokenAccount, 110 | isSigner: false, 111 | isWritable: false, 112 | }, 113 | { 114 | pubkey: newMintAccount, 115 | isSigner: false, 116 | isWritable: false, 117 | }, 118 | { 119 | pubkey: SystemProgram.programId, 120 | isSigner: false, 121 | isWritable: false, 122 | }, 123 | { 124 | pubkey: SYSVAR_RENT_PUBKEY, 125 | isSigner: false, 126 | isWritable: false, 127 | }, 128 | ]; 129 | return new TransactionInstruction({ 130 | programId, 131 | keys, 132 | data: txnData, 133 | }); 134 | } 135 | 136 | export function createAssociatedTokenAccountInstruction( 137 | associatedTokenAddress: PublicKey, 138 | payer: PublicKey, 139 | walletAddress: PublicKey, 140 | splTokenMintAddress: PublicKey, 141 | ) { 142 | const keys = [ 143 | { 144 | pubkey: payer, 145 | isSigner: true, 146 | isWritable: true, 147 | }, 148 | { 149 | pubkey: associatedTokenAddress, 150 | isSigner: false, 151 | isWritable: true, 152 | }, 153 | { 154 | pubkey: walletAddress, 155 | isSigner: false, 156 | isWritable: false, 157 | }, 158 | { 159 | pubkey: splTokenMintAddress, 160 | isSigner: false, 161 | isWritable: false, 162 | }, 163 | { 164 | pubkey: SystemProgram.programId, 165 | isSigner: false, 166 | isWritable: false, 167 | }, 168 | { 169 | pubkey: TOKEN_PROGRAM_ID, 170 | isSigner: false, 171 | isWritable: false, 172 | }, 173 | { 174 | pubkey: SYSVAR_RENT_PUBKEY, 175 | isSigner: false, 176 | isWritable: false, 177 | }, 178 | ]; 179 | return new TransactionInstruction({ 180 | keys, 181 | programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 182 | data: Buffer.from([]), 183 | }); 184 | } 185 | 186 | 187 | export function createMetadataInstruction( 188 | metadataAccount: PublicKey, 189 | mint: PublicKey, 190 | mintAuthority: PublicKey, 191 | payer: PublicKey, 192 | updateAuthority: PublicKey, 193 | txnData: Buffer, 194 | ) { 195 | const keys = [ 196 | { 197 | pubkey: metadataAccount, 198 | isSigner: false, 199 | isWritable: true, 200 | }, 201 | { 202 | pubkey: mint, 203 | isSigner: false, 204 | isWritable: false, 205 | }, 206 | { 207 | pubkey: mintAuthority, 208 | isSigner: true, 209 | isWritable: false, 210 | }, 211 | { 212 | pubkey: payer, 213 | isSigner: true, 214 | isWritable: false, 215 | }, 216 | { 217 | pubkey: updateAuthority, 218 | isSigner: false, 219 | isWritable: false, 220 | }, 221 | { 222 | pubkey: SystemProgram.programId, 223 | isSigner: false, 224 | isWritable: false, 225 | }, 226 | { 227 | pubkey: SYSVAR_RENT_PUBKEY, 228 | isSigner: false, 229 | isWritable: false, 230 | }, 231 | ]; 232 | return new TransactionInstruction({ 233 | keys, 234 | programId: TOKEN_METADATA_PROGRAM_ID, 235 | data: txnData, 236 | }); 237 | } 238 | 239 | export function createMasterEditionInstruction( 240 | metadataAccount: PublicKey, 241 | editionAccount: PublicKey, 242 | mint: PublicKey, 243 | mintAuthority: PublicKey, 244 | payer: PublicKey, 245 | updateAuthority: PublicKey, 246 | txnData: Buffer, 247 | ) { 248 | 249 | const keys = [ 250 | { 251 | pubkey: editionAccount, 252 | isSigner: false, 253 | isWritable: true, 254 | }, 255 | { 256 | pubkey: mint, 257 | isSigner: false, 258 | isWritable: true, 259 | }, 260 | { 261 | pubkey: updateAuthority, 262 | isSigner: true, 263 | isWritable: false, 264 | }, 265 | { 266 | pubkey: mintAuthority, 267 | isSigner: true, 268 | isWritable: false, 269 | }, 270 | { 271 | pubkey: payer, 272 | isSigner: true, 273 | isWritable: false, 274 | }, 275 | { 276 | pubkey: metadataAccount, 277 | isSigner: false, 278 | isWritable: false, 279 | }, 280 | { 281 | pubkey: TOKEN_PROGRAM_ID, 282 | isSigner: false, 283 | isWritable: false, 284 | }, 285 | { 286 | pubkey: SystemProgram.programId, 287 | isSigner: false, 288 | isWritable: false, 289 | }, 290 | { 291 | pubkey: SYSVAR_RENT_PUBKEY, 292 | isSigner: false, 293 | isWritable: false, 294 | }, 295 | ]; 296 | return new TransactionInstruction({ 297 | keys, 298 | programId: TOKEN_METADATA_PROGRAM_ID, 299 | data: txnData, 300 | }); 301 | } 302 | 303 | export async function createConfigAccount( 304 | anchorProgram, 305 | configData, 306 | payerWallet, 307 | configAccount, 308 | ) { 309 | const size = 310 | CONFIG_ARRAY_START + 311 | 4 + 312 | configData.maxNumberOfLines.toNumber() * CONFIG_LINE_SIZE + 313 | 4 + 314 | Math.ceil(configData.maxNumberOfLines.toNumber() / 8); 315 | 316 | return anchor.web3.SystemProgram.createAccount({ 317 | fromPubkey: payerWallet, 318 | newAccountPubkey: configAccount, 319 | space: size, 320 | lamports: 321 | await anchorProgram.provider.connection.getMinimumBalanceForRentExemption( 322 | size, 323 | ), 324 | programId: CANDY_MACHINE_PROGRAM_ID, 325 | }); 326 | } 327 | -------------------------------------------------------------------------------- /cli/helpers/transactions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Blockhash, 3 | Commitment, 4 | Connection, 5 | FeeCalculator, 6 | Keypair, 7 | RpcResponseAndContext, 8 | SignatureStatus, 9 | SimulatedTransactionResponse, 10 | Transaction, 11 | TransactionInstruction, 12 | TransactionSignature, 13 | } from '@solana/web3.js'; 14 | import { getUnixTs, sleep } from './various'; 15 | import { DEFAULT_TIMEOUT } from './constants'; 16 | import log from 'loglevel'; 17 | 18 | interface BlockhashAndFeeCalculator { 19 | blockhash: Blockhash; 20 | feeCalculator: FeeCalculator; 21 | } 22 | 23 | export const sendTransactionWithRetryWithKeypair = async ( 24 | connection: Connection, 25 | wallet: Keypair, 26 | instructions: TransactionInstruction[], 27 | signers: Keypair[], 28 | commitment: Commitment = 'singleGossip', 29 | includesFeePayer: boolean = false, 30 | block?: BlockhashAndFeeCalculator, 31 | beforeSend?: () => void, 32 | ) => { 33 | const transaction = new Transaction(); 34 | instructions.forEach(instruction => transaction.add(instruction)); 35 | transaction.recentBlockhash = ( 36 | block || (await connection.getRecentBlockhash(commitment)) 37 | ).blockhash; 38 | 39 | if (includesFeePayer) { 40 | transaction.setSigners(...signers.map(s => s.publicKey)); 41 | } else { 42 | transaction.setSigners( 43 | // fee payed by the wallet owner 44 | wallet.publicKey, 45 | ...signers.map(s => s.publicKey), 46 | ); 47 | } 48 | 49 | if (signers.length > 0) { 50 | transaction.sign(...[wallet, ...signers]); 51 | } else { 52 | transaction.sign(wallet); 53 | } 54 | 55 | if (beforeSend) { 56 | beforeSend(); 57 | } 58 | 59 | const { txid, slot } = await sendSignedTransaction({ 60 | connection, 61 | signedTransaction: transaction, 62 | }); 63 | 64 | return { txid, slot }; 65 | }; 66 | 67 | export async function sendSignedTransaction({ 68 | signedTransaction, 69 | connection, 70 | timeout = DEFAULT_TIMEOUT, 71 | }: { 72 | signedTransaction: Transaction; 73 | connection: Connection; 74 | sendingMessage?: string; 75 | sentMessage?: string; 76 | successMessage?: string; 77 | timeout?: number; 78 | }): Promise<{ txid: string; slot: number }> { 79 | const rawTransaction = signedTransaction.serialize(); 80 | const startTime = getUnixTs(); 81 | let slot = 0; 82 | const txid: TransactionSignature = await connection.sendRawTransaction( 83 | rawTransaction, 84 | { 85 | skipPreflight: true, 86 | }, 87 | ); 88 | 89 | log.debug('Started awaiting confirmation for', txid); 90 | 91 | let done = false; 92 | (async () => { 93 | while (!done && getUnixTs() - startTime < timeout) { 94 | connection.sendRawTransaction(rawTransaction, { 95 | skipPreflight: true, 96 | }); 97 | await sleep(500); 98 | } 99 | })(); 100 | try { 101 | const confirmation = await awaitTransactionSignatureConfirmation( 102 | txid, 103 | timeout, 104 | connection, 105 | 'recent', 106 | true, 107 | ); 108 | 109 | if (!confirmation) 110 | throw new Error('Timed out awaiting confirmation on transaction'); 111 | 112 | if (confirmation.err) { 113 | log.error(confirmation.err); 114 | throw new Error('Transaction failed: Custom instruction error'); 115 | } 116 | 117 | slot = confirmation?.slot || 0; 118 | } catch (err) { 119 | log.error('Timeout Error caught', err); 120 | if (err.timeout) { 121 | throw new Error('Timed out awaiting confirmation on transaction'); 122 | } 123 | let simulateResult: SimulatedTransactionResponse | null = null; 124 | try { 125 | simulateResult = ( 126 | await simulateTransaction(connection, signedTransaction, 'single') 127 | ).value; 128 | } catch (e) { 129 | log.error('Simulate Transaction error', e); 130 | } 131 | if (simulateResult && simulateResult.err) { 132 | if (simulateResult.logs) { 133 | for (let i = simulateResult.logs.length - 1; i >= 0; --i) { 134 | const line = simulateResult.logs[i]; 135 | if (line.startsWith('Program log: ')) { 136 | throw new Error( 137 | 'Transaction failed: ' + line.slice('Program log: '.length), 138 | ); 139 | } 140 | } 141 | } 142 | throw new Error(JSON.stringify(simulateResult.err)); 143 | } 144 | // throw new Error('Transaction failed'); 145 | } finally { 146 | done = true; 147 | } 148 | 149 | log.debug('Latency', txid, getUnixTs() - startTime); 150 | return { txid, slot }; 151 | } 152 | 153 | async function simulateTransaction( 154 | connection: Connection, 155 | transaction: Transaction, 156 | commitment: Commitment, 157 | ): Promise> { 158 | // @ts-ignore 159 | transaction.recentBlockhash = await connection._recentBlockhash( 160 | // @ts-ignore 161 | connection._disableBlockhashCaching, 162 | ); 163 | 164 | const signData = transaction.serializeMessage(); 165 | // @ts-ignore 166 | const wireTransaction = transaction._serialize(signData); 167 | const encodedTransaction = wireTransaction.toString('base64'); 168 | const config: any = { encoding: 'base64', commitment }; 169 | const args = [encodedTransaction, config]; 170 | 171 | // @ts-ignore 172 | const res = await connection._rpcRequest('simulateTransaction', args); 173 | if (res.error) { 174 | throw new Error('failed to simulate transaction: ' + res.error.message); 175 | } 176 | return res.result; 177 | } 178 | 179 | async function awaitTransactionSignatureConfirmation( 180 | txid: TransactionSignature, 181 | timeout: number, 182 | connection: Connection, 183 | commitment: Commitment = 'recent', 184 | queryStatus = false, 185 | ): Promise { 186 | let done = false; 187 | let status: SignatureStatus | null | void = { 188 | slot: 0, 189 | confirmations: 0, 190 | err: null, 191 | }; 192 | let subId = 0; 193 | // eslint-disable-next-line no-async-promise-executor 194 | status = await new Promise(async (resolve, reject) => { 195 | setTimeout(() => { 196 | if (done) { 197 | return; 198 | } 199 | done = true; 200 | log.warn('Rejecting for timeout...'); 201 | reject({ timeout: true }); 202 | }, timeout); 203 | try { 204 | subId = connection.onSignature( 205 | txid, 206 | (result, context) => { 207 | done = true; 208 | status = { 209 | err: result.err, 210 | slot: context.slot, 211 | confirmations: 0, 212 | }; 213 | if (result.err) { 214 | log.warn('Rejected via websocket', result.err); 215 | reject(status); 216 | } else { 217 | log.debug('Resolved via websocket', result); 218 | resolve(status); 219 | } 220 | }, 221 | commitment, 222 | ); 223 | } catch (e) { 224 | done = true; 225 | log.error('WS error in setup', txid, e); 226 | } 227 | while (!done && queryStatus) { 228 | // eslint-disable-next-line no-loop-func 229 | (async () => { 230 | try { 231 | const signatureStatuses = await connection.getSignatureStatuses([ 232 | txid, 233 | ]); 234 | status = signatureStatuses && signatureStatuses.value[0]; 235 | if (!done) { 236 | if (!status) { 237 | log.debug('REST null result for', txid, status); 238 | } else if (status.err) { 239 | log.error('REST error for', txid, status); 240 | done = true; 241 | reject(status.err); 242 | } else if (!status.confirmations) { 243 | log.error('REST no confirmations for', txid, status); 244 | } else { 245 | log.debug('REST confirmation for', txid, status); 246 | done = true; 247 | resolve(status); 248 | } 249 | } 250 | } catch (e) { 251 | if (!done) { 252 | log.error('REST connection error: txid', txid, e); 253 | } 254 | } 255 | })(); 256 | await sleep(2000); 257 | } 258 | }); 259 | 260 | //@ts-ignore 261 | if (connection._signatureSubscriptions[subId]) 262 | connection.removeSignatureListener(subId); 263 | done = true; 264 | log.debug('Returning status', status); 265 | return status; 266 | } 267 | -------------------------------------------------------------------------------- /cli/cli-hero.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | import * as dotenv from "dotenv"; 3 | import * as fs from 'fs'; 4 | import { program } from 'commander'; 5 | import log from 'loglevel'; 6 | import { web3 } from '@project-serum/anchor'; 7 | 8 | import { createNewHero } from './commands/createHero'; 9 | import { updateHero } from './commands/updateHero'; 10 | import { purchaseNFT } from './commands/purchaseHero'; 11 | import { upload } from './commands/upload'; 12 | import { getAllHeros } from './commands/fetchAll'; 13 | 14 | import { loadWalletKey } from './helpers/accounts'; 15 | import { 16 | parsePrice, 17 | } from './helpers/various'; 18 | import { 19 | EXTENSION_JPG, 20 | EXTENSION_PNG, 21 | } from './helpers/constants'; 22 | 23 | dotenv.config({ path: __dirname+'/.env' }); 24 | 25 | program.version('0.0.1'); 26 | log.setLevel('info'); 27 | 28 | programCommand('create_hero') 29 | .option('-n, --name ', 'hero name') 30 | .option('-u, --uri ', 'hero image') 31 | .option('-p, --price ', 'hero price') 32 | .option('-o, --owner ', 'owner nft mint address') 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | .action(async (directory, cmd) => { 35 | const { 36 | keypair, 37 | env, 38 | name, 39 | uri, 40 | price, 41 | owner, 42 | } = cmd.opts(); 43 | 44 | let parsedPrice = parsePrice(price); 45 | if (price && isNaN(parsedPrice)) { 46 | throw new Error(`Price is not valid. Please input as valid float type.`); 47 | } 48 | 49 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 50 | const programId = process.env.HERO_METADATA_PROGRAM_ID; 51 | log.info(`Hero program Id: ${programId.toString()}`); 52 | if (!programId) { 53 | throw new Error(`Hero Program Id is not provided in .env file`); 54 | } 55 | const walletKeyPair = loadWalletKey(keypair); 56 | log.info(`create_hero: n-${name}, u-${uri}, p-${parsedPrice}, o-${owner}`); 57 | await createNewHero(solConnection, programId, walletKeyPair, {name, uri, price: parsedPrice, ownerNftAddress: owner}); 58 | }); 59 | 60 | programCommand('show_all') 61 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 62 | .action(async (directory, cmd) => { 63 | const { 64 | keypair, 65 | env, 66 | } = cmd.opts(); 67 | 68 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 69 | const programId = process.env.HERO_METADATA_PROGRAM_ID; 70 | if (!programId) { 71 | throw new Error(`Hero Program Id is not provided in .env file`); 72 | } 73 | log.info(`show_all: e-${env} env-${programId}`); 74 | const heroList = await getAllHeros(solConnection, programId); 75 | log.info(heroList); 76 | }); 77 | 78 | programCommand('update_hero_price') 79 | .option('-i, --id ', 'hero Id') 80 | .option('-p, --price ', 'new hero price') 81 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 82 | .action(async (directory, cmd) => { 83 | const { 84 | keypair, 85 | env, 86 | id, 87 | price, 88 | } = cmd.opts(); 89 | 90 | let parsedPrice = parsePrice(price); 91 | if (price && isNaN(parsedPrice)) { 92 | throw new Error(`Price is not valid. Please input as valid float type.`); 93 | } 94 | 95 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 96 | const programId = process.env.HERO_METADATA_PROGRAM_ID; 97 | if (!programId) { 98 | throw new Error(`Hero Program Id is not provided in .env file`); 99 | } 100 | const walletKeyPair = loadWalletKey(keypair); 101 | log.info(`update_hero_price: i-${id}, p-${parsedPrice}`); 102 | await updateHero(solConnection, programId, walletKeyPair, id, parsedPrice); 103 | }); 104 | 105 | programCommand('buy_hero') 106 | .option('-i, --id ', 'hero Id') 107 | .option('-n, --name ', 'new hero name') 108 | .option('-u, --uri ', 'new hero image') 109 | .option('-p, --price ', 'new hero price as Sol') 110 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 111 | .action(async (directory, cmd) => { 112 | const { 113 | keypair, 114 | env, 115 | id, 116 | name, 117 | uri, 118 | price, 119 | } = cmd.opts(); 120 | 121 | let parsedPrice = parsePrice(price); 122 | if (price && isNaN(parsedPrice)) { 123 | throw new Error(`Price is not valid. Please input as valid float type.`); 124 | } 125 | 126 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 127 | const programId = process.env.HERO_METADATA_PROGRAM_ID; 128 | if (!programId) { 129 | throw new Error(`Hero Program Id is not provided in .env file`); 130 | } 131 | const walletKeyPair = loadWalletKey(keypair); 132 | let wallet = walletKeyPair.publicKey; 133 | log.info(`buy_hero: i-${id}, n-${name}, u-${uri}, p-${parsedPrice}`); 134 | await purchaseNFT(solConnection, programId, env, walletKeyPair, id, name, uri, parsedPrice); 135 | }); 136 | 137 | programCommand('upload_image') 138 | .argument( 139 | '', 140 | 'Image file path to upload', 141 | ) 142 | .option( 143 | '-s, --storage ', 144 | 'Database to use for storage (arweave, ipfs, aws)', 145 | 'arweave', 146 | ) 147 | .option( 148 | '--ipfs-infura-project-id ', 149 | 'Infura IPFS project id (required if using IPFS)', 150 | ) 151 | .option( 152 | '--ipfs-infura-secret ', 153 | 'Infura IPFS scret key (required if using IPFS)', 154 | ) 155 | .option( 156 | '--aws-s3-bucket ', 157 | '(existing) AWS S3 Bucket name (required if using aws)', 158 | ) 159 | .action(async (imgFile: string, options, cmd) => { 160 | if(!fs.existsSync(imgFile)) { 161 | throw new Error(`Image file not exist. Please check the image path.`); 162 | } 163 | 164 | const { 165 | keypair, 166 | env, 167 | storage, 168 | ipfsInfuraProjectId, 169 | ipfsInfuraSecret, 170 | awsS3Bucket, 171 | } = cmd.opts(); 172 | 173 | if (storage === 'ipfs' && (!ipfsInfuraProjectId || !ipfsInfuraSecret)) { 174 | throw new Error( 175 | 'IPFS selected as storage option but Infura project id or secret key were not provided.', 176 | ); 177 | } 178 | if (storage === 'aws' && !awsS3Bucket) { 179 | throw new Error( 180 | 'aws selected as storage option but existing bucket name (--aws-s3-bucket) not provided.', 181 | ); 182 | } 183 | if (!(storage === 'arweave' || storage === 'ipfs' || storage === 'aws')) { 184 | throw new Error( 185 | "Storage option must either be 'arweave', 'ipfs', or 'aws'.", 186 | ); 187 | } 188 | const ipfsCredentials = { 189 | projectId: ipfsInfuraProjectId, 190 | secretKey: ipfsInfuraSecret, 191 | }; 192 | 193 | const isPngFile = imgFile.endsWith(EXTENSION_PNG); 194 | const isJpgFile = imgFile.endsWith(EXTENSION_JPG); 195 | 196 | if (!isPngFile && !isJpgFile) { 197 | throw new Error( 198 | `Image extension should be png or jpg.`, 199 | ); 200 | } 201 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 202 | 203 | log.info(`Beginning the upload for ${isJpgFile ? `jpg` : `png`} image file`); 204 | 205 | const startMs = Date.now(); 206 | log.info('started at: ' + startMs.toString()); 207 | let warn = false; 208 | for (;;) { 209 | const successful = await upload( 210 | solConnection, 211 | imgFile, 212 | env, 213 | keypair, 214 | storage, 215 | ipfsCredentials, 216 | awsS3Bucket, 217 | ); 218 | 219 | if (successful) { 220 | warn = false; 221 | break; 222 | } else { 223 | warn = true; 224 | log.warn('upload was not successful, rerunning'); 225 | } 226 | } 227 | const endMs = Date.now(); 228 | const timeTaken = new Date(endMs - startMs).toISOString().substr(11, 8); 229 | log.info( 230 | `ended at: ${new Date(endMs).toISOString()}. time taken: ${timeTaken}`, 231 | ); 232 | if (warn) { 233 | log.info('not all images have been uploaded, rerun this step.'); 234 | } 235 | }); 236 | 237 | function programCommand(name: string) { 238 | return program 239 | .command(name) 240 | .option( 241 | '-e, --env ', 242 | 'Solana cluster env name', 243 | 'devnet', //mainnet-beta, testnet, devnet 244 | ) 245 | .option( 246 | '-k, --keypair ', 247 | `Solana wallet location`, 248 | '--keypair not provided', 249 | ) 250 | .option('-l, --log-level ', 'log level', setLogLevel); 251 | } 252 | 253 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 254 | function setLogLevel(value, prev) { 255 | if (value === undefined || value === null) { 256 | return; 257 | } 258 | log.info('setting the log value to: ' + value); 259 | log.setLevel(value); 260 | } 261 | 262 | program.parse(process.argv); 263 | -------------------------------------------------------------------------------- /src/hero_script.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@project-serum/anchor"; 2 | 3 | import { 4 | MintLayout, 5 | TOKEN_PROGRAM_ID, 6 | Token, 7 | } from "@solana/spl-token"; 8 | 9 | export const CANDY_MACHINE_PROGRAM = new anchor.web3.PublicKey( 10 | "cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ" 11 | ); 12 | 13 | const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new anchor.web3.PublicKey( 14 | "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 15 | ); 16 | 17 | const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey( 18 | "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" 19 | ); 20 | 21 | export interface CandyMachine { 22 | id: anchor.web3.PublicKey, 23 | connection: anchor.web3.Connection; 24 | program: anchor.Program; 25 | } 26 | 27 | interface CandyMachineState { 28 | candyMachine: CandyMachine; 29 | itemsAvailable: number; 30 | itemsRedeemed: number; 31 | itemsRemaining: number; 32 | goLiveDate: Date, 33 | } 34 | 35 | export const awaitTransactionSignatureConfirmation = async ( 36 | txid: anchor.web3.TransactionSignature, 37 | timeout: number, 38 | connection: anchor.web3.Connection, 39 | commitment: anchor.web3.Commitment = "recent", 40 | queryStatus = false 41 | ): Promise => { 42 | let done = false; 43 | let status: anchor.web3.SignatureStatus | null | void = { 44 | slot: 0, 45 | confirmations: 0, 46 | err: null, 47 | }; 48 | let subId = 0; 49 | status = await new Promise(async (resolve, reject) => { 50 | setTimeout(() => { 51 | if (done) { 52 | return; 53 | } 54 | done = true; 55 | console.log("Rejecting for timeout..."); 56 | reject({ timeout: true }); 57 | }, timeout); 58 | try { 59 | subId = connection.onSignature( 60 | txid, 61 | (result: any, context: any) => { 62 | done = true; 63 | status = { 64 | err: result.err, 65 | slot: context.slot, 66 | confirmations: 0, 67 | }; 68 | if (result.err) { 69 | console.log("Rejected via websocket", result.err); 70 | reject(status); 71 | } else { 72 | console.log("Resolved via websocket", result); 73 | resolve(status); 74 | } 75 | }, 76 | commitment 77 | ); 78 | } catch (e) { 79 | done = true; 80 | console.error("WS error in setup", txid, e); 81 | } 82 | while (!done && queryStatus) { 83 | // eslint-disable-next-line no-loop-func 84 | (async () => { 85 | try { 86 | const signatureStatuses = await connection.getSignatureStatuses([ 87 | txid, 88 | ]); 89 | status = signatureStatuses && signatureStatuses.value[0]; 90 | if (!done) { 91 | if (!status) { 92 | console.log("REST null result for", txid, status); 93 | } else if (status.err) { 94 | console.log("REST error for", txid, status); 95 | done = true; 96 | reject(status.err); 97 | } else if (!status.confirmations) { 98 | console.log("REST no confirmations for", txid, status); 99 | } else { 100 | console.log("REST confirmation for", txid, status); 101 | done = true; 102 | resolve(status); 103 | } 104 | } 105 | } catch (e) { 106 | if (!done) { 107 | console.log("REST connection error: txid", txid, e); 108 | } 109 | } 110 | })(); 111 | await sleep(2000); 112 | } 113 | }); 114 | 115 | //@ts-ignore 116 | if (connection._signatureSubscriptions[subId]) { 117 | connection.removeSignatureListener(subId); 118 | } 119 | done = true; 120 | console.log("Returning status", status); 121 | return status; 122 | } 123 | 124 | export const createAssociatedTokenAccountInstruction = ( 125 | associatedTokenAddress: anchor.web3.PublicKey, 126 | payer: anchor.web3.PublicKey, 127 | walletAddress: anchor.web3.PublicKey, 128 | splTokenMintAddress: anchor.web3.PublicKey 129 | ) => { 130 | const keys = [ 131 | { pubkey: payer, isSigner: true, isWritable: true }, 132 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 133 | { pubkey: walletAddress, isSigner: false, isWritable: false }, 134 | { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, 135 | { 136 | pubkey: anchor.web3.SystemProgram.programId, 137 | isSigner: false, 138 | isWritable: false, 139 | }, 140 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 141 | { 142 | pubkey: anchor.web3.SYSVAR_RENT_PUBKEY, 143 | isSigner: false, 144 | isWritable: false, 145 | }, 146 | ]; 147 | return new anchor.web3.TransactionInstruction({ 148 | keys, 149 | programId: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 150 | data: Buffer.from([]), 151 | }); 152 | } 153 | 154 | export const getCandyMachineState = async ( 155 | anchorWallet: anchor.Wallet, 156 | candyMachineId: anchor.web3.PublicKey, 157 | connection: anchor.web3.Connection, 158 | ): Promise => { 159 | const provider = new anchor.Provider(connection, anchorWallet, { 160 | preflightCommitment: "recent", 161 | }); 162 | 163 | const idl = await anchor.Program.fetchIdl( 164 | CANDY_MACHINE_PROGRAM, 165 | provider 166 | ); 167 | 168 | const program = new anchor.Program(idl, CANDY_MACHINE_PROGRAM, provider); 169 | const candyMachine = { 170 | id: candyMachineId, 171 | connection, 172 | program, 173 | } 174 | 175 | const state: any = await program.account.candyMachine.fetch(candyMachineId); 176 | 177 | const itemsAvailable = state.data.itemsAvailable.toNumber(); 178 | const itemsRedeemed = state.itemsRedeemed.toNumber(); 179 | const itemsRemaining = itemsAvailable - itemsRedeemed; 180 | 181 | let goLiveDate = state.data.goLiveDate.toNumber(); 182 | goLiveDate = new Date(goLiveDate * 1000); 183 | 184 | console.log({ 185 | itemsAvailable, 186 | itemsRedeemed, 187 | itemsRemaining, 188 | goLiveDate, 189 | }) 190 | 191 | return { 192 | candyMachine, 193 | itemsAvailable, 194 | itemsRedeemed, 195 | itemsRemaining, 196 | goLiveDate, 197 | }; 198 | } 199 | 200 | const getMasterEdition = async ( 201 | mint: anchor.web3.PublicKey 202 | ): Promise => { 203 | return ( 204 | await anchor.web3.PublicKey.findProgramAddress( 205 | [ 206 | Buffer.from("metadata"), 207 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 208 | mint.toBuffer(), 209 | Buffer.from("edition"), 210 | ], 211 | TOKEN_METADATA_PROGRAM_ID 212 | ) 213 | )[0]; 214 | }; 215 | 216 | const getMetadata = async ( 217 | mint: anchor.web3.PublicKey 218 | ): Promise => { 219 | return ( 220 | await anchor.web3.PublicKey.findProgramAddress( 221 | [ 222 | Buffer.from("metadata"), 223 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 224 | mint.toBuffer(), 225 | ], 226 | TOKEN_METADATA_PROGRAM_ID 227 | ) 228 | )[0]; 229 | }; 230 | 231 | export const getTokenWallet = async ( 232 | wallet: anchor.web3.PublicKey, 233 | mint: anchor.web3.PublicKey 234 | ) => { 235 | return ( 236 | await anchor.web3.PublicKey.findProgramAddress( 237 | [wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 238 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID 239 | ) 240 | )[0]; 241 | }; 242 | 243 | export const mintOneToken = async ( 244 | candyMachine: CandyMachine, 245 | config: anchor.web3.PublicKey, // feels like this should be part of candyMachine? 246 | payer: anchor.web3.PublicKey, 247 | treasury: anchor.web3.PublicKey, 248 | ): Promise => { 249 | console.log(candyMachine); 250 | const mint = anchor.web3.Keypair.generate(); 251 | const token = await getTokenWallet(payer, mint.publicKey); 252 | const { connection, program } = candyMachine; 253 | const metadata = await getMetadata(mint.publicKey); 254 | const masterEdition = await getMasterEdition(mint.publicKey); 255 | 256 | const rent = await connection.getMinimumBalanceForRentExemption( 257 | MintLayout.span 258 | ); 259 | 260 | return await program.rpc.mintNft({ 261 | accounts: { 262 | config, 263 | candyMachine: candyMachine.id, 264 | payer: payer, 265 | wallet: treasury, 266 | mint: mint.publicKey, 267 | metadata, 268 | masterEdition, 269 | mintAuthority: payer, 270 | updateAuthority: payer, 271 | tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, 272 | tokenProgram: TOKEN_PROGRAM_ID, 273 | systemProgram: anchor.web3.SystemProgram.programId, 274 | rent: anchor.web3.SYSVAR_RENT_PUBKEY, 275 | clock: anchor.web3.SYSVAR_CLOCK_PUBKEY, 276 | }, 277 | signers: [mint], 278 | instructions: [ 279 | anchor.web3.SystemProgram.createAccount({ 280 | fromPubkey: payer, 281 | newAccountPubkey: mint.publicKey, 282 | space: MintLayout.span, 283 | lamports: rent, 284 | programId: TOKEN_PROGRAM_ID, 285 | }), 286 | Token.createInitMintInstruction( 287 | TOKEN_PROGRAM_ID, 288 | mint.publicKey, 289 | 0, 290 | payer, 291 | payer 292 | ), 293 | createAssociatedTokenAccountInstruction( 294 | token, 295 | payer, 296 | payer, 297 | mint.publicKey 298 | ), 299 | Token.createMintToInstruction( 300 | TOKEN_PROGRAM_ID, 301 | mint.publicKey, 302 | token, 303 | payer, 304 | [], 305 | 1 306 | ), 307 | ], 308 | }); 309 | } 310 | 311 | export const shortenAddress = (address: string, chars = 4): string => { 312 | return `${address.slice(0, chars)}...${address.slice(-chars)}`; 313 | }; 314 | 315 | const sleep = (ms: number): Promise => { 316 | return new Promise((resolve) => setTimeout(resolve, ms)); 317 | } -------------------------------------------------------------------------------- /cli/commands/purchaseHero.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAssociatedTokenAccountInstruction, 3 | createMetadataInstruction, 4 | purchaseHeroInstruction, 5 | createMasterEditionInstruction, 6 | } from '../helpers/instructions'; 7 | import { sendTransactionWithRetryWithKeypair } from '../helpers/transactions'; 8 | import { 9 | getTokenWallet, 10 | getMetadata, 11 | getMasterEdition, 12 | getHeroDataKey, 13 | } from '../helpers/accounts'; 14 | import * as anchor from '@project-serum/anchor'; 15 | import { 16 | Data, 17 | Herodata, 18 | Creator, 19 | CreateMetadataArgs, 20 | PurchaseHeroArgs, 21 | CreateMasterEditionArgs, 22 | METADATA_SCHEMA, 23 | } from '../helpers/schema'; 24 | import { serialize } from 'borsh'; 25 | import { getProgramAccounts, decodeHeroMetadata } from './fetchAll'; 26 | import { uploadMeta } from './upload'; 27 | import { TOKEN_PROGRAM_ID } from '../helpers/constants'; 28 | import { AccountLayout, MintLayout, u64, Token } from '@solana/spl-token'; 29 | import { 30 | Keypair, 31 | Connection, 32 | SystemProgram, 33 | TransactionInstruction, 34 | PublicKey, 35 | } from '@solana/web3.js'; 36 | import BN from 'bn.js'; 37 | import log from 'loglevel'; 38 | import { sleep } from '../helpers/various'; 39 | 40 | export const purchaseNFT = async ( 41 | connection: Connection, 42 | heroProgramAddress: string, 43 | env: string, 44 | walletKeypair: Keypair, 45 | id: number, 46 | new_name: string, 47 | new_uri: string, 48 | new_price: number, 49 | ): Promise<{ 50 | metadataAccount: PublicKey; 51 | } | void> => { 52 | // Validate heroData 53 | if ( 54 | new_price && isNaN(new_price) 55 | ) { 56 | log.error('Invalid new_price', new_price); 57 | return; 58 | } 59 | 60 | // Create wallet from keypair 61 | const wallet = new anchor.Wallet(walletKeypair); 62 | if (!wallet?.publicKey) return; 63 | 64 | const programId = new PublicKey(heroProgramAddress); 65 | 66 | const instructions: TransactionInstruction[] = []; 67 | const signers: anchor.web3.Keypair[] = [walletKeypair]; 68 | 69 | // Update metadata 70 | let herodataAccount = await getHeroDataKey(id, programId); 71 | log.info(`Generated hero account: ${herodataAccount}`); 72 | 73 | const result = await getProgramAccounts( 74 | connection, 75 | heroProgramAddress, 76 | {}, 77 | ); 78 | const count = result.length; 79 | log.info(`Fetched hero counts: ${count}`); 80 | if (id > count) { 81 | log.error('Invalid id ', count); 82 | return; 83 | } 84 | 85 | let ownerNftAddress: PublicKey; 86 | let heroData: Herodata; 87 | for(let hero of result) { 88 | const accountPubkey = hero.pubkey; 89 | if (accountPubkey == herodataAccount.toBase58()) { 90 | const decoded: Herodata = await decodeHeroMetadata(hero.account.data); 91 | ownerNftAddress = new PublicKey(decoded.ownerNftAddress); 92 | heroData = decoded; 93 | break; 94 | } 95 | }; 96 | log.info(`Retrived owner nft address: ${ownerNftAddress}`); 97 | 98 | const fetchData = await getProgramAccounts( 99 | connection, 100 | TOKEN_PROGRAM_ID.toBase58(), 101 | { 102 | filters: [ 103 | { 104 | memcmp: { 105 | offset: 0, 106 | bytes: ownerNftAddress.toBase58(), 107 | }, 108 | }, 109 | { 110 | dataSize: 165 111 | }, 112 | ], 113 | }, 114 | ); 115 | let accountPubkey: string; 116 | let accountOwnerPubkey: string; 117 | for(let token of fetchData) { 118 | accountPubkey = token.pubkey; 119 | let accountData = deserializeAccount(token.account.data); 120 | if (accountData.amount == 1) { 121 | accountOwnerPubkey = accountData.owner; 122 | break; 123 | } 124 | }; 125 | log.info(`Token account address: ${accountPubkey}`); 126 | log.info(`Token account owner: ${accountOwnerPubkey}`); 127 | 128 | let txnData = Buffer.from( 129 | serialize( 130 | METADATA_SCHEMA, 131 | new PurchaseHeroArgs({ 132 | id, 133 | name: new_name ? new_name : null, 134 | uri: new_uri ? new_uri : null, 135 | price: !new_price || new_price == NaN ? null : new BN(new_price), 136 | }), 137 | ), 138 | ); 139 | 140 | // Generate a mint 141 | const mint = anchor.web3.Keypair.generate(); 142 | signers.push(mint); 143 | 144 | // Allocate memory for the account 145 | const mintRent = await connection.getMinimumBalanceForRentExemption( 146 | MintLayout.span, 147 | ); 148 | 149 | instructions.push( 150 | SystemProgram.createAccount({ 151 | fromPubkey: wallet.publicKey, 152 | newAccountPubkey: mint.publicKey, 153 | lamports: mintRent, 154 | space: MintLayout.span, 155 | programId: TOKEN_PROGRAM_ID, 156 | }), 157 | ); 158 | 159 | instructions.push( 160 | purchaseHeroInstruction( 161 | herodataAccount, 162 | wallet.publicKey, 163 | new PublicKey(accountOwnerPubkey), 164 | new PublicKey(accountPubkey), 165 | mint.publicKey, 166 | txnData, 167 | programId, 168 | ), 169 | ); 170 | 171 | let name = Buffer.from(heroData.name); 172 | name = name.slice(0, name.indexOf(0)); 173 | let uri = Buffer.from(heroData.uri); 174 | uri = uri.slice(0, uri.indexOf(0)); 175 | 176 | let metadata = { 177 | "name": new_name ? new_name : name.toString(), 178 | "image": new_uri ? new_uri : uri.toString(), 179 | "symbol": '', 180 | "seller_fee_basis_points": 0, 181 | "description": "", 182 | "collection": {}, 183 | "attributes": [], 184 | "properties": { 185 | "files": [ 186 | { 187 | "uri": new_uri ? new_uri : uri.toString(), 188 | "type": "image/png" 189 | } 190 | ], 191 | "category": "image", 192 | "creators": [ 193 | { 194 | "address": wallet.publicKey.toBase58(), 195 | "share": 100, 196 | }, 197 | { 198 | "address": programId.toBase58(), 199 | "share": 0, 200 | } 201 | ], 202 | } 203 | }; 204 | 205 | let warn = false; 206 | let metadata_uri; 207 | for (;;) { 208 | const { 209 | status, 210 | link 211 | } = await uploadMeta( 212 | connection, 213 | metadata, 214 | env, 215 | walletKeypair, 216 | ); 217 | 218 | if (status) { 219 | warn = false; 220 | metadata_uri = link; 221 | break; 222 | } else { 223 | warn = true; 224 | log.warn('upload was not successful, rerunning'); 225 | } 226 | } 227 | log.info(`Uploaded metadata to Arweave`); 228 | sleep(2000); 229 | 230 | // Validate metadata 231 | if ( 232 | !metadata.name || 233 | !metadata.image || 234 | isNaN(metadata.seller_fee_basis_points) || 235 | !metadata.properties 236 | ) { 237 | log.error('Invalid metadata file', metadata); 238 | return; 239 | } 240 | 241 | instructions.push( 242 | Token.createInitMintInstruction( 243 | TOKEN_PROGRAM_ID, 244 | mint.publicKey, 245 | 0, 246 | wallet.publicKey, 247 | wallet.publicKey, 248 | ), 249 | ); 250 | 251 | const userTokenAccoutAddress = await getTokenWallet( 252 | wallet.publicKey, 253 | mint.publicKey, 254 | ); 255 | instructions.push( 256 | createAssociatedTokenAccountInstruction( 257 | userTokenAccoutAddress, 258 | wallet.publicKey, 259 | wallet.publicKey, 260 | mint.publicKey, 261 | ), 262 | ); 263 | 264 | // Create metadata 265 | const metadataAccount = await getMetadata(mint.publicKey); 266 | const creators = [ 267 | new Creator({ 268 | address: wallet.publicKey.toBase58(), 269 | share: 100, 270 | verified: 1, 271 | }), 272 | new Creator({ 273 | address: programId.toBase58(), 274 | share: 0, 275 | verified: 0, 276 | }), 277 | ]; 278 | const data = new Data({ 279 | symbol: metadata.symbol, 280 | name: metadata.name, 281 | uri: metadata_uri, 282 | sellerFeeBasisPoints: metadata.seller_fee_basis_points, 283 | creators: creators, 284 | }); 285 | log.info(data); 286 | 287 | let nftTxnData = Buffer.from( 288 | serialize( 289 | METADATA_SCHEMA, 290 | new CreateMetadataArgs({ data, isMutable: false }), 291 | ), 292 | ); 293 | 294 | instructions.push( 295 | createMetadataInstruction( 296 | metadataAccount, 297 | mint.publicKey, 298 | wallet.publicKey, 299 | wallet.publicKey, 300 | wallet.publicKey, 301 | nftTxnData, 302 | ), 303 | ); 304 | 305 | instructions.push( 306 | Token.createMintToInstruction( 307 | TOKEN_PROGRAM_ID, 308 | mint.publicKey, 309 | userTokenAccoutAddress, 310 | wallet.publicKey, 311 | [], 312 | 1, 313 | ), 314 | ); 315 | 316 | // Create master edition 317 | const editionAccount = await getMasterEdition(mint.publicKey); 318 | nftTxnData = Buffer.from( 319 | serialize( 320 | METADATA_SCHEMA, 321 | new CreateMasterEditionArgs({ maxSupply: new BN(0) }), 322 | ), 323 | ); 324 | 325 | instructions.push( 326 | createMasterEditionInstruction( 327 | metadataAccount, 328 | editionAccount, 329 | mint.publicKey, 330 | wallet.publicKey, 331 | wallet.publicKey, 332 | wallet.publicKey, 333 | nftTxnData, 334 | ), 335 | ); 336 | 337 | const res = await sendTransactionWithRetryWithKeypair( 338 | connection, 339 | walletKeypair, 340 | instructions, 341 | signers, 342 | ); 343 | 344 | try { 345 | await connection.confirmTransaction(res.txid, 'max'); 346 | } catch { 347 | // ignore 348 | } 349 | 350 | // Force wait for max confirmations 351 | await connection.getParsedConfirmedTransaction(res.txid, 'confirmed'); 352 | log.info('Purchase NFT minted', res.txid); 353 | return { metadataAccount }; 354 | }; 355 | 356 | export const deserializeAccount = (data: Buffer) => { 357 | const accountInfo = AccountLayout.decode(data); 358 | accountInfo.mint = new PublicKey(accountInfo.mint); 359 | accountInfo.owner = new PublicKey(accountInfo.owner); 360 | accountInfo.amount = u64.fromBuffer(accountInfo.amount); 361 | 362 | return accountInfo; 363 | }; -------------------------------------------------------------------------------- /cli/helpers/accounts.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; 2 | import { 3 | CANDY_MACHINE, 4 | CANDY_MACHINE_PROGRAM_ID, 5 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 6 | TOKEN_METADATA_PROGRAM_ID, 7 | TOKEN_PROGRAM_ID, 8 | FAIR_LAUNCH_PROGRAM_ID, 9 | } from './constants'; 10 | import * as anchor from '@project-serum/anchor'; 11 | import fs from 'fs'; 12 | import BN from 'bn.js'; 13 | import { createConfigAccount } from './instructions'; 14 | import { web3 } from '@project-serum/anchor'; 15 | import log from 'loglevel'; 16 | 17 | export const createConfig = async function ( 18 | anchorProgram: anchor.Program, 19 | payerWallet: Keypair, 20 | configData: { 21 | maxNumberOfLines: BN; 22 | symbol: string; 23 | sellerFeeBasisPoints: number; 24 | isMutable: boolean; 25 | maxSupply: BN; 26 | retainAuthority: boolean; 27 | creators: { 28 | address: PublicKey; 29 | verified: boolean; 30 | share: number; 31 | }[]; 32 | }, 33 | ) { 34 | const configAccount = Keypair.generate(); 35 | const uuid = uuidFromConfigPubkey(configAccount.publicKey); 36 | 37 | return { 38 | config: configAccount.publicKey, 39 | uuid, 40 | txId: await anchorProgram.rpc.initializeConfig( 41 | { 42 | uuid, 43 | ...configData, 44 | }, 45 | { 46 | accounts: { 47 | config: configAccount.publicKey, 48 | authority: payerWallet.publicKey, 49 | payer: payerWallet.publicKey, 50 | systemProgram: SystemProgram.programId, 51 | rent: anchor.web3.SYSVAR_RENT_PUBKEY, 52 | }, 53 | signers: [payerWallet, configAccount], 54 | instructions: [ 55 | await createConfigAccount( 56 | anchorProgram, 57 | configData, 58 | payerWallet.publicKey, 59 | configAccount.publicKey, 60 | ), 61 | ], 62 | }, 63 | ), 64 | }; 65 | }; 66 | 67 | export function uuidFromConfigPubkey(configAccount: PublicKey) { 68 | return configAccount.toBase58().slice(0, 6); 69 | } 70 | 71 | export const getTokenWallet = async function ( 72 | wallet: PublicKey, 73 | mint: PublicKey, 74 | ) { 75 | return ( 76 | await PublicKey.findProgramAddress( 77 | [wallet.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 78 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 79 | ) 80 | )[0]; 81 | }; 82 | 83 | export const getCandyMachineAddress = async ( 84 | config: anchor.web3.PublicKey, 85 | uuid: string, 86 | ): Promise<[PublicKey, number]> => { 87 | return await anchor.web3.PublicKey.findProgramAddress( 88 | [Buffer.from(CANDY_MACHINE), config.toBuffer(), Buffer.from(uuid)], 89 | CANDY_MACHINE_PROGRAM_ID, 90 | ); 91 | }; 92 | 93 | export const getConfig = async ( 94 | authority: anchor.web3.PublicKey, 95 | uuid: string, 96 | ): Promise<[PublicKey, number]> => { 97 | return await anchor.web3.PublicKey.findProgramAddress( 98 | [Buffer.from(CANDY_MACHINE), authority.toBuffer(), Buffer.from(uuid)], 99 | CANDY_MACHINE_PROGRAM_ID, 100 | ); 101 | }; 102 | 103 | export const getTokenMint = async ( 104 | authority: anchor.web3.PublicKey, 105 | uuid: string, 106 | ): Promise<[anchor.web3.PublicKey, number]> => { 107 | return await anchor.web3.PublicKey.findProgramAddress( 108 | [ 109 | Buffer.from('fair_launch'), 110 | authority.toBuffer(), 111 | Buffer.from('mint'), 112 | Buffer.from(uuid), 113 | ], 114 | FAIR_LAUNCH_PROGRAM_ID, 115 | ); 116 | }; 117 | 118 | export const getFairLaunch = async ( 119 | tokenMint: anchor.web3.PublicKey, 120 | ): Promise<[anchor.web3.PublicKey, number]> => { 121 | return await anchor.web3.PublicKey.findProgramAddress( 122 | [Buffer.from('fair_launch'), tokenMint.toBuffer()], 123 | FAIR_LAUNCH_PROGRAM_ID, 124 | ); 125 | }; 126 | 127 | export const getFairLaunchTicket = async ( 128 | tokenMint: anchor.web3.PublicKey, 129 | buyer: anchor.web3.PublicKey, 130 | ): Promise<[anchor.web3.PublicKey, number]> => { 131 | return await anchor.web3.PublicKey.findProgramAddress( 132 | [Buffer.from('fair_launch'), tokenMint.toBuffer(), buyer.toBuffer()], 133 | FAIR_LAUNCH_PROGRAM_ID, 134 | ); 135 | }; 136 | 137 | export const getFairLaunchLotteryBitmap = async ( 138 | tokenMint: anchor.web3.PublicKey, 139 | ): Promise<[anchor.web3.PublicKey, number]> => { 140 | return await anchor.web3.PublicKey.findProgramAddress( 141 | [Buffer.from('fair_launch'), tokenMint.toBuffer(), Buffer.from('lottery')], 142 | FAIR_LAUNCH_PROGRAM_ID, 143 | ); 144 | }; 145 | 146 | export const getFairLaunchTicketSeqLookup = async ( 147 | tokenMint: anchor.web3.PublicKey, 148 | seq: anchor.BN, 149 | ): Promise<[anchor.web3.PublicKey, number]> => { 150 | return await anchor.web3.PublicKey.findProgramAddress( 151 | [Buffer.from('fair_launch'), tokenMint.toBuffer(), seq.toBuffer('le', 8)], 152 | FAIR_LAUNCH_PROGRAM_ID, 153 | ); 154 | }; 155 | 156 | export const getAtaForMint = async ( 157 | mint: anchor.web3.PublicKey, 158 | buyer: anchor.web3.PublicKey, 159 | ): Promise<[anchor.web3.PublicKey, number]> => { 160 | return await anchor.web3.PublicKey.findProgramAddress( 161 | [buyer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 162 | SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, 163 | ); 164 | }; 165 | 166 | export const getParticipationMint = async ( 167 | authority: anchor.web3.PublicKey, 168 | uuid: string, 169 | ): Promise<[anchor.web3.PublicKey, number]> => { 170 | return await anchor.web3.PublicKey.findProgramAddress( 171 | [ 172 | Buffer.from('fair_launch'), 173 | authority.toBuffer(), 174 | Buffer.from('mint'), 175 | Buffer.from(uuid), 176 | Buffer.from('participation'), 177 | ], 178 | FAIR_LAUNCH_PROGRAM_ID, 179 | ); 180 | }; 181 | 182 | export const getParticipationToken = async ( 183 | authority: anchor.web3.PublicKey, 184 | uuid: string, 185 | ): Promise<[anchor.web3.PublicKey, number]> => { 186 | return await anchor.web3.PublicKey.findProgramAddress( 187 | [ 188 | Buffer.from('fair_launch'), 189 | authority.toBuffer(), 190 | Buffer.from('mint'), 191 | Buffer.from(uuid), 192 | Buffer.from('participation'), 193 | Buffer.from('account'), 194 | ], 195 | FAIR_LAUNCH_PROGRAM_ID, 196 | ); 197 | }; 198 | 199 | export const getTreasury = async ( 200 | tokenMint: anchor.web3.PublicKey, 201 | ): Promise<[anchor.web3.PublicKey, number]> => { 202 | return await anchor.web3.PublicKey.findProgramAddress( 203 | [Buffer.from('fair_launch'), tokenMint.toBuffer(), Buffer.from('treasury')], 204 | FAIR_LAUNCH_PROGRAM_ID, 205 | ); 206 | }; 207 | 208 | export const getMetadata = async ( 209 | mint: anchor.web3.PublicKey, 210 | ): Promise => { 211 | return ( 212 | await anchor.web3.PublicKey.findProgramAddress( 213 | [ 214 | Buffer.from('metadata'), 215 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 216 | mint.toBuffer(), 217 | ], 218 | TOKEN_METADATA_PROGRAM_ID, 219 | ) 220 | )[0]; 221 | }; 222 | 223 | export const getHeroDataKey = async ( 224 | id: number, 225 | programId: PublicKey, 226 | ): Promise => { 227 | return ( 228 | await anchor.web3.PublicKey.findProgramAddress( 229 | [ 230 | Buffer.from('metadata'), 231 | programId.toBuffer(), 232 | Buffer.from([id]), 233 | ], 234 | programId, 235 | ) 236 | )[0]; 237 | }; 238 | 239 | export const getMasterEdition = async ( 240 | mint: anchor.web3.PublicKey, 241 | ): Promise => { 242 | return ( 243 | await anchor.web3.PublicKey.findProgramAddress( 244 | [ 245 | Buffer.from('metadata'), 246 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 247 | mint.toBuffer(), 248 | Buffer.from('edition'), 249 | ], 250 | TOKEN_METADATA_PROGRAM_ID, 251 | ) 252 | )[0]; 253 | }; 254 | 255 | export const getEditionMarkPda = async ( 256 | mint: anchor.web3.PublicKey, 257 | edition: number, 258 | ): Promise => { 259 | const editionNumber = Math.floor(edition / 248); 260 | return ( 261 | await anchor.web3.PublicKey.findProgramAddress( 262 | [ 263 | Buffer.from('metadata'), 264 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 265 | mint.toBuffer(), 266 | Buffer.from('edition'), 267 | Buffer.from(editionNumber.toString()), 268 | ], 269 | TOKEN_METADATA_PROGRAM_ID, 270 | ) 271 | )[0]; 272 | }; 273 | 274 | export function loadWalletKey(keypair): Keypair { 275 | if (!keypair || keypair == '') { 276 | throw new Error('Keypair is required!'); 277 | } 278 | const loaded = Keypair.fromSecretKey( 279 | new Uint8Array(JSON.parse(fs.readFileSync(keypair).toString())), 280 | ); 281 | log.info(`wallet public key: ${loaded.publicKey}`); 282 | return loaded; 283 | } 284 | 285 | export async function loadCandyProgram(walletKeyPair: Keypair, env: string) { 286 | // @ts-ignore 287 | const solConnection = new web3.Connection(web3.clusterApiUrl(env)); 288 | const walletWrapper = new anchor.Wallet(walletKeyPair); 289 | const provider = new anchor.Provider(solConnection, walletWrapper, { 290 | preflightCommitment: 'recent', 291 | }); 292 | const idl = await anchor.Program.fetchIdl(CANDY_MACHINE_PROGRAM_ID, provider); 293 | 294 | const program = new anchor.Program(idl, CANDY_MACHINE_PROGRAM_ID, provider); 295 | log.debug('program id from anchor', program.programId.toBase58()); 296 | return program; 297 | } 298 | 299 | export async function loadFairLaunchProgram( 300 | walletKeyPair: Keypair, 301 | env: string, 302 | customRpcUrl?: string, 303 | ) { 304 | if (customRpcUrl) console.log('USING CUSTOM URL', customRpcUrl); 305 | 306 | // @ts-ignore 307 | const solConnection = new anchor.web3.Connection( 308 | //@ts-ignore 309 | customRpcUrl || web3.clusterApiUrl(env), 310 | ); 311 | const walletWrapper = new anchor.Wallet(walletKeyPair); 312 | const provider = new anchor.Provider(solConnection, walletWrapper, { 313 | preflightCommitment: 'recent', 314 | }); 315 | const idl = await anchor.Program.fetchIdl(FAIR_LAUNCH_PROGRAM_ID, provider); 316 | 317 | return new anchor.Program(idl, FAIR_LAUNCH_PROGRAM_ID, provider); 318 | } 319 | -------------------------------------------------------------------------------- /rust/token-metadata/program/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use { 4 | num_derive::FromPrimitive, 5 | solana_program::{ 6 | decode_error::DecodeError, 7 | msg, 8 | program_error::{PrintProgramError, ProgramError}, 9 | }, 10 | thiserror::Error, 11 | }; 12 | 13 | /// Errors that may be returned by the Metadata program. 14 | #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] 15 | pub enum MetadataError { 16 | /// Failed to unpack instruction data 17 | #[error("Failed to unpack instruction data")] 18 | InstructionUnpackError, 19 | 20 | /// Failed to pack instruction data 21 | #[error("Failed to pack instruction data")] 22 | InstructionPackError, 23 | 24 | /// Lamport balance below rent-exempt threshold. 25 | #[error("Lamport balance below rent-exempt threshold")] 26 | NotRentExempt, 27 | 28 | /// Already initialized 29 | #[error("Already initialized")] 30 | AlreadyInitialized, 31 | 32 | /// Uninitialized 33 | #[error("Uninitialized")] 34 | Uninitialized, 35 | 36 | /// Metadata's key must match seed of ['metadata', program id, mint] provided 37 | #[error(" Metadata's key must match seed of ['metadata', program id, mint] provided")] 38 | InvalidMetadataKey, 39 | 40 | /// Edition's key must match seed of ['metadata', program id, name, 'edition'] provided 41 | #[error("Edition's key must match seed of ['metadata', program id, name, 'edition'] provided")] 42 | InvalidEditionKey, 43 | 44 | /// Update Authority given does not match 45 | #[error("Update Authority given does not match")] 46 | UpdateAuthorityIncorrect, 47 | 48 | /// Update Authority needs to be signer to update metadata 49 | #[error("Update Authority needs to be signer to update metadata")] 50 | UpdateAuthorityIsNotSigner, 51 | 52 | /// You must be the mint authority and signer on this transaction 53 | #[error("You must be the mint authority and signer on this transaction")] 54 | NotMintAuthority, 55 | 56 | /// Mint authority provided does not match the authority on the mint 57 | #[error("Mint authority provided does not match the authority on the mint")] 58 | InvalidMintAuthority, 59 | 60 | /// Name too long 61 | #[error("Name too long")] 62 | NameTooLong, 63 | 64 | /// Symbol too long 65 | #[error("Symbol too long")] 66 | SymbolTooLong, 67 | 68 | /// URI too long 69 | #[error("URI too long")] 70 | UriTooLong, 71 | 72 | /// Update authority must be equivalent to the metadata's authority and also signer of this transaction 73 | #[error("Update authority must be equivalent to the metadata's authority and also signer of this transaction")] 74 | UpdateAuthorityMustBeEqualToMetadataAuthorityAndSigner, 75 | 76 | /// Mint given does not match mint on Metadata 77 | #[error("Mint given does not match mint on Metadata")] 78 | MintMismatch, 79 | 80 | /// Editions must have exactly one token 81 | #[error("Editions must have exactly one token")] 82 | EditionsMustHaveExactlyOneToken, 83 | 84 | /// Maximum editions printed already 85 | #[error("Maximum editions printed already")] 86 | MaxEditionsMintedAlready, 87 | 88 | /// Token mint to failed 89 | #[error("Token mint to failed")] 90 | TokenMintToFailed, 91 | 92 | /// The master edition record passed must match the master record on the edition given 93 | #[error("The master edition record passed must match the master record on the edition given")] 94 | MasterRecordMismatch, 95 | 96 | /// The destination account does not have the right mint 97 | #[error("The destination account does not have the right mint")] 98 | DestinationMintMismatch, 99 | 100 | /// An edition can only mint one of its kind! 101 | #[error("An edition can only mint one of its kind!")] 102 | EditionAlreadyMinted, 103 | 104 | /// Printing mint decimals should be zero 105 | #[error("Printing mint decimals should be zero")] 106 | PrintingMintDecimalsShouldBeZero, 107 | 108 | /// OneTimePrintingAuthorizationMint mint decimals should be zero 109 | #[error("OneTimePrintingAuthorization mint decimals should be zero")] 110 | OneTimePrintingAuthorizationMintDecimalsShouldBeZero, 111 | 112 | /// Edition mint decimals should be zero 113 | #[error("EditionMintDecimalsShouldBeZero")] 114 | EditionMintDecimalsShouldBeZero, 115 | 116 | /// Token burn failed 117 | #[error("Token burn failed")] 118 | TokenBurnFailed, 119 | 120 | /// The One Time authorization mint does not match that on the token account! 121 | #[error("The One Time authorization mint does not match that on the token account!")] 122 | TokenAccountOneTimeAuthMintMismatch, 123 | 124 | /// Derived key invalid 125 | #[error("Derived key invalid")] 126 | DerivedKeyInvalid, 127 | 128 | /// The Printing mint does not match that on the master edition! 129 | #[error("The Printing mint does not match that on the master edition!")] 130 | PrintingMintMismatch, 131 | 132 | /// The One Time Printing Auth mint does not match that on the master edition! 133 | #[error("The One Time Printing Auth mint does not match that on the master edition!")] 134 | OneTimePrintingAuthMintMismatch, 135 | 136 | /// The mint of the token account does not match the Printing mint! 137 | #[error("The mint of the token account does not match the Printing mint!")] 138 | TokenAccountMintMismatch, 139 | 140 | /// The mint of the token account does not match the master metadata mint! 141 | #[error("The mint of the token account does not match the master metadata mint!")] 142 | TokenAccountMintMismatchV2, 143 | 144 | /// Not enough tokens to mint a limited edition 145 | #[error("Not enough tokens to mint a limited edition")] 146 | NotEnoughTokens, 147 | 148 | /// The mint on your authorization token holding account does not match your Printing mint! 149 | #[error( 150 | "The mint on your authorization token holding account does not match your Printing mint!" 151 | )] 152 | PrintingMintAuthorizationAccountMismatch, 153 | 154 | /// The authorization token account has a different owner than the update authority for the master edition! 155 | #[error("The authorization token account has a different owner than the update authority for the master edition!")] 156 | AuthorizationTokenAccountOwnerMismatch, 157 | 158 | /// This feature is currently disabled. 159 | #[error("This feature is currently disabled.")] 160 | Disabled, 161 | 162 | /// Creators list too long 163 | #[error("Creators list too long")] 164 | CreatorsTooLong, 165 | 166 | /// Creators must be at least one if set 167 | #[error("Creators must be at least one if set")] 168 | CreatorsMustBeAtleastOne, 169 | 170 | /// If using a creators array, you must be one of the creators listed 171 | #[error("If using a creators array, you must be one of the creators listed")] 172 | MustBeOneOfCreators, 173 | 174 | /// This metadata does not have creators 175 | #[error("This metadata does not have creators")] 176 | NoCreatorsPresentOnMetadata, 177 | 178 | /// This creator address was not found 179 | #[error("This creator address was not found")] 180 | CreatorNotFound, 181 | 182 | /// Basis points cannot be more than 10000 183 | #[error("Basis points cannot be more than 10000")] 184 | InvalidBasisPoints, 185 | 186 | /// Primary sale can only be flipped to true and is immutable 187 | #[error("Primary sale can only be flipped to true and is immutable")] 188 | PrimarySaleCanOnlyBeFlippedToTrue, 189 | 190 | /// Owner does not match that on the account given 191 | #[error("Owner does not match that on the account given")] 192 | OwnerMismatch, 193 | 194 | /// This account has no tokens to be used for authorization 195 | #[error("This account has no tokens to be used for authorization")] 196 | NoBalanceInAccountForAuthorization, 197 | 198 | /// Share total must equal 100 for creator array 199 | #[error("Share total must equal 100 for creator array")] 200 | ShareTotalMustBe100, 201 | 202 | /// This reservation list already exists! 203 | #[error("This reservation list already exists!")] 204 | ReservationExists, 205 | 206 | /// This reservation list does not exist! 207 | #[error("This reservation list does not exist!")] 208 | ReservationDoesNotExist, 209 | 210 | /// This reservation list exists but was never set with reservations 211 | #[error("This reservation list exists but was never set with reservations")] 212 | ReservationNotSet, 213 | 214 | /// This reservation list has already been set! 215 | #[error("This reservation list has already been set!")] 216 | ReservationAlreadyMade, 217 | 218 | /// Provided more addresses than max allowed in single reservation 219 | #[error("Provided more addresses than max allowed in single reservation")] 220 | BeyondMaxAddressSize, 221 | 222 | /// NumericalOverflowError 223 | #[error("NumericalOverflowError")] 224 | NumericalOverflowError, 225 | 226 | /// This reservation would go beyond the maximum supply of the master edition! 227 | #[error("This reservation would go beyond the maximum supply of the master edition!")] 228 | ReservationBreachesMaximumSupply, 229 | 230 | /// Address not in reservation! 231 | #[error("Address not in reservation!")] 232 | AddressNotInReservation, 233 | 234 | /// You cannot unilaterally verify another creator, they must sign 235 | #[error("You cannot unilaterally verify another creator, they must sign")] 236 | CannotVerifyAnotherCreator, 237 | 238 | /// You cannot unilaterally unverify another creator 239 | #[error("You cannot unilaterally unverify another creator")] 240 | CannotUnverifyAnotherCreator, 241 | 242 | /// In initial reservation setting, spots remaining should equal total spots 243 | #[error("In initial reservation setting, spots remaining should equal total spots")] 244 | SpotMismatch, 245 | 246 | /// Incorrect account owner 247 | #[error("Incorrect account owner")] 248 | IncorrectOwner, 249 | 250 | /// printing these tokens would breach the maximum supply limit of the master edition 251 | #[error("printing these tokens would breach the maximum supply limit of the master edition")] 252 | PrintingWouldBreachMaximumSupply, 253 | 254 | /// Data is immutable 255 | #[error("Data is immutable")] 256 | DataIsImmutable, 257 | 258 | /// No duplicate creator addresses 259 | #[error("No duplicate creator addresses")] 260 | DuplicateCreatorAddress, 261 | 262 | /// Reservation spots remaining should match total spots when first being created 263 | #[error("Reservation spots remaining should match total spots when first being created")] 264 | ReservationSpotsRemainingShouldMatchTotalSpotsAtStart, 265 | 266 | /// Invalid token program 267 | #[error("Invalid token program")] 268 | InvalidTokenProgram, 269 | 270 | /// Data type mismatch 271 | #[error("Data type mismatch")] 272 | DataTypeMismatch, 273 | 274 | /// Beyond alotted address size in reservation! 275 | #[error("Beyond alotted address size in reservation!")] 276 | BeyondAlottedAddressSize, 277 | 278 | /// The reservation has only been partially alotted 279 | #[error("The reservation has only been partially alotted")] 280 | ReservationNotComplete, 281 | 282 | /// You cannot splice over an existing reservation! 283 | #[error("You cannot splice over an existing reservation!")] 284 | TriedToReplaceAnExistingReservation, 285 | 286 | /// Invalid operation 287 | #[error("Invalid operation")] 288 | InvalidOperation, 289 | 290 | /// Invalid owner 291 | #[error("Invalid Owner")] 292 | InvalidOwner, 293 | 294 | /// Printing mint supply must be zero for conversion 295 | #[error("Printing mint supply must be zero for conversion")] 296 | PrintingMintSupplyMustBeZeroForConversion, 297 | 298 | /// One Time Auth mint supply must be zero for conversion 299 | #[error("One Time Auth mint supply must be zero for conversion")] 300 | OneTimeAuthMintSupplyMustBeZeroForConversion, 301 | 302 | /// You tried to insert one edition too many into an edition mark pda 303 | #[error("You tried to insert one edition too many into an edition mark pda")] 304 | InvalidEditionIndex, 305 | 306 | // In the legacy system the reservation needs to be of size one for cpu limit reasons 307 | #[error("In the legacy system the reservation needs to be of size one for cpu limit reasons")] 308 | ReservationArrayShouldBeSizeOne, 309 | } 310 | 311 | impl PrintProgramError for MetadataError { 312 | fn print(&self) { 313 | msg!(&self.to_string()); 314 | } 315 | } 316 | 317 | impl From for ProgramError { 318 | fn from(e: MetadataError) -> Self { 319 | ProgramError::Custom(e as u32) 320 | } 321 | } 322 | 323 | impl DecodeError for MetadataError { 324 | fn type_of() -> &'static str { 325 | "Metadata Error" 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import styled from "styled-components"; 3 | import Countdown from "react-countdown"; 4 | import axios from 'axios'; 5 | import { Button, CircularProgress, Snackbar } from "@material-ui/core"; 6 | import Alert from "@material-ui/lab/Alert"; 7 | 8 | import * as anchor from "@project-serum/anchor"; 9 | 10 | import { LAMPORTS_PER_SOL, PublicKey, Transaction } from "@solana/web3.js"; 11 | 12 | import { useAnchorWallet } from "@solana/wallet-adapter-react"; 13 | import { WalletDialogButton } from "@solana/wallet-adapter-material-ui"; 14 | 15 | import { 16 | CandyMachine, 17 | awaitTransactionSignatureConfirmation, 18 | getCandyMachineState, 19 | mintOneToken, 20 | shortenAddress, 21 | getTokenWallet, 22 | createAssociatedTokenAccountInstruction, 23 | } from "./hero_script"; 24 | 25 | import Skeleton from './logo.svg'; 26 | import { randomBytes } from "crypto"; 27 | import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 28 | import { getParsedNftAccountsByOwner } from '@nfteyez/sol-rayz'; 29 | 30 | const OWNER_WALLET_PUBKEY = 'E5GSUDTQAvJouZkxHFGMA3THVzXWvrs4hRZEag2au3k6';//'51QHr8aS4En232fPCWUYLxWYw4crwxeap56n4jF1283Y'; 31 | 32 | const ConnectButton = styled(WalletDialogButton)``; 33 | 34 | const CounterText = styled.span``; // add your styles here 35 | 36 | const MintContainer = styled.div``; // add your styles here 37 | 38 | const MintButton = styled(Button)``; // add your styles here 39 | 40 | export interface HomeProps { 41 | candyMachineId: anchor.web3.PublicKey; 42 | config: anchor.web3.PublicKey; 43 | connection: anchor.web3.Connection; 44 | startDate: number; 45 | treasury: anchor.web3.PublicKey; 46 | txTimeout: number; 47 | } 48 | 49 | interface NFTItem { 50 | pubkey: string, 51 | uri: string | undefined, 52 | } 53 | 54 | const indexes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; 55 | 56 | const items = [ 57 | {pubkey: '1', uri: undefined}, 58 | {pubkey: '2', uri: undefined}, 59 | {pubkey: '3', uri: undefined}, 60 | {pubkey: '4', uri: undefined}, 61 | {pubkey: '5', uri: undefined}, 62 | {pubkey: '6', uri: undefined}, 63 | {pubkey: '7', uri: undefined}, 64 | {pubkey: '8', uri: undefined}, 65 | {pubkey: '9', uri: undefined}, 66 | {pubkey: '10', uri: undefined}, 67 | {pubkey: '11', uri: undefined}, 68 | {pubkey: '12', uri: undefined}, 69 | ]; 70 | 71 | const Home = (props: HomeProps) => { 72 | const [curPage, setCurPage] = useState(0); 73 | const [curItems, setCurItems] = useState(items); 74 | const [stakedItems, setStakedItems] = useState(items); 75 | const [balance, setBalance] = useState(); 76 | const [isActive, setIsActive] = useState(false); // true when countdown completes 77 | const [isSoldOut, setIsSoldOut] = useState(false); // true when items remaining is zero 78 | const [isMinting, setIsMinting] = useState(false); // true when user got to press MINT 79 | 80 | const [itemsAvailable, setItemsAvailable] = useState(0); 81 | const [itemsRedeemed, setItemsRedeemed] = useState(0); 82 | const [itemsRemaining, setItemsRemaining] = useState(0); 83 | 84 | const [alertState, setAlertState] = useState({ 85 | open: false, 86 | message: "", 87 | severity: undefined, 88 | }); 89 | 90 | const [startDate, setStartDate] = useState(new Date(props.startDate)); 91 | 92 | const wallet = useAnchorWallet(); 93 | const [candyMachine, setCandyMachine] = useState(); 94 | 95 | const refreshCandyMachineState = () => { 96 | (async () => { 97 | if (!wallet) return; 98 | 99 | const { 100 | candyMachine, 101 | goLiveDate, 102 | itemsAvailable, 103 | itemsRemaining, 104 | itemsRedeemed, 105 | } = await getCandyMachineState( 106 | wallet as anchor.Wallet, 107 | props.candyMachineId, 108 | props.connection 109 | ); 110 | 111 | setItemsAvailable(itemsAvailable); 112 | setItemsRemaining(itemsRemaining); 113 | setItemsRedeemed(itemsRedeemed); 114 | 115 | setIsSoldOut(itemsRemaining === 0); 116 | setStartDate(goLiveDate); 117 | setCandyMachine(candyMachine); 118 | })(); 119 | }; 120 | 121 | const onMint = async (index: number) => { 122 | try { 123 | setIsMinting(true); 124 | if (wallet && candyMachine?.program) { 125 | if (curItems.length <= index) throw `Out of item index range`; 126 | 127 | console.log(`--> Transfer ${curItems[index].pubkey}\ 128 | from ${wallet.publicKey.toString()} to ${OWNER_WALLET_PUBKEY}`); 129 | 130 | const connection = props.connection; 131 | const tokenMintPubkey = new PublicKey(curItems[index].pubkey); 132 | const sourcePubkey = await getTokenWallet(wallet.publicKey, tokenMintPubkey); 133 | const destinationPubkey = await getTokenWallet(new PublicKey(OWNER_WALLET_PUBKEY), tokenMintPubkey); 134 | 135 | const createATAIx = createAssociatedTokenAccountInstruction( 136 | destinationPubkey, 137 | wallet.publicKey, 138 | new PublicKey(OWNER_WALLET_PUBKEY), 139 | tokenMintPubkey, 140 | ); 141 | 142 | const transferIx = Token.createTransferInstruction( 143 | TOKEN_PROGRAM_ID, 144 | sourcePubkey, 145 | destinationPubkey, 146 | wallet.publicKey, 147 | [], 148 | 1 149 | ); 150 | 151 | const instructions = [createATAIx, transferIx]; 152 | 153 | let tx = new Transaction().add(...instructions); 154 | tx.setSigners( 155 | ...([wallet.publicKey]), 156 | ); 157 | tx.recentBlockhash = (await connection.getRecentBlockhash("max")).blockhash; 158 | let signed = await wallet.signTransaction(tx); 159 | let txid = await connection.sendRawTransaction(signed.serialize(), { 160 | skipPreflight: false, 161 | preflightCommitment: 'singleGossip' 162 | }); 163 | const transferTxId = await connection.confirmTransaction(txid, 'singleGossip'); 164 | 165 | let newItems = Object.assign(curItems); 166 | newItems.splice(index, 1); 167 | setCurItems(newItems); 168 | 169 | const result = await axios.post('http://localhost:4040/api/claim-staked', { 170 | address: wallet.publicKey.toBase58(), 171 | }); 172 | console.log(result); 173 | 174 | // const mintTxId = await mintOneToken( 175 | // candyMachine, 176 | // props.config, 177 | // wallet.publicKey, 178 | // props.treasury 179 | // ); 180 | const status = transferTxId.value; 181 | // const status = await awaitTransactionSignatureConfirmation( 182 | // mintTxId, 183 | // props.txTimeout, 184 | // props.connection, 185 | // "singleGossip", 186 | // false 187 | // ); 188 | 189 | if (!status?.err) { 190 | setAlertState({ 191 | open: true, 192 | message: "Congratulations! Mint succeeded!", 193 | severity: "success", 194 | }); 195 | } else { 196 | setAlertState({ 197 | open: true, 198 | message: "Mint failed! Please try again!", 199 | severity: "error", 200 | }); 201 | } 202 | } 203 | } catch (error: any) { 204 | // TODO: blech: 205 | let message = error.msg || error.message || "Minting failed! Please try again!"; 206 | if (!error.msg) { 207 | if (error.message.indexOf("0x138")) { 208 | } else if (error.message.indexOf("0x137")) { 209 | message = `SOLD OUT!`; 210 | } else if (error.message.indexOf("0x135")) { 211 | message = `Insufficient funds to mint. Please fund your wallet.`; 212 | } 213 | } else { 214 | if (error.code === 311) { 215 | message = `SOLD OUT!`; 216 | setIsSoldOut(true); 217 | } else if (error.code === 312) { 218 | message = `Minting period hasn't started yet.`; 219 | } 220 | } 221 | console.log(error); 222 | setAlertState({ 223 | open: true, 224 | message, 225 | severity: "error", 226 | }); 227 | } finally { 228 | if (wallet) { 229 | const balance = await props.connection.getBalance(wallet.publicKey); 230 | setBalance(balance / LAMPORTS_PER_SOL); 231 | } 232 | setIsMinting(false); 233 | // refreshCandyMachineState(); 234 | } 235 | }; 236 | 237 | useEffect(() => { 238 | (async () => { 239 | if (wallet) { 240 | const balance = await props.connection.getBalance(wallet.publicKey); 241 | setBalance(balance / LAMPORTS_PER_SOL); 242 | const nftAccounts = await getParsedNftAccountsByOwner({ 243 | publicAddress: wallet.publicKey, 244 | connection: props.connection, 245 | }) 246 | if (nftAccounts.length > 0) { 247 | // Parse transaction and get raw wallet activities 248 | let nfts = [] as any, parsedCount = 0; 249 | const parsedNFTs = await Promise.allSettled( 250 | nftAccounts.map((account: any, index: number) => { 251 | axios.get(account.data.uri).then((result) => { 252 | if(nfts.length > index) nfts[index].uri = result.data.image; 253 | parsedCount++; 254 | }).catch (err => { 255 | console.log(err); // eslint-disable-line 256 | parsedCount++; 257 | }); 258 | return { 259 | pubkey: account.mint, 260 | uri: account.data.uri, //result.data.image ?? 'https://picsum.photos/200/200', 261 | }; 262 | }) 263 | ); 264 | 265 | nfts = parsedNFTs 266 | .filter(({ status }) => status === "fulfilled") 267 | .flatMap((p) => (p as PromiseFulfilledResult).value); 268 | 269 | let interval = 0 as any; 270 | interval = setInterval(() => { 271 | if(parsedCount == nftAccounts.length) clearInterval(interval); 272 | setCurItems(nfts); 273 | }, 500); 274 | } 275 | } else { 276 | setCurItems(curItems); 277 | setCurPage(0); 278 | } 279 | })(); 280 | }, [wallet, props.connection]); 281 | 282 | useEffect(refreshCandyMachineState, [ 283 | wallet, 284 | props.candyMachineId, 285 | props.connection, 286 | ]); 287 | 288 | return ( 289 |
290 |
298 | {wallet && { 299 | setCurPage(0); 300 | }}>Home} 301 | {wallet && { 302 | setCurPage(1); 303 | }}>My heroes} 304 |
305 | {wallet && ( 306 |
307 |
308 |

Total Available: {itemsAvailable}

309 |

Redeemed: {itemsRedeemed}

310 |

Remaining: {itemsRemaining}

311 |
312 |
313 |

Wallet {shortenAddress(wallet.publicKey.toBase58() || "")}

314 |

Balance: {(balance || 0).toLocaleString()} SOL

315 |
316 |
317 | )} 318 | 319 | 320 | {!wallet ? ( 321 | <> 322 | Connect Wallet 323 |
331 | { indexes.map((item, idx) => 332 |
339 |
345 | {'back' 355 |
356 | {}} 359 | variant="contained" 360 | > 361 | MINT 362 | 363 |
364 | )} 365 |
366 | 367 | ) : 368 |
377 | {(curPage == 0 ? curItems : stakedItems).map((item, idx) => 378 |
385 |
391 | {'' 402 | {item.pubkey} 403 | {'back' 416 |
417 | {onMint(idx)}} 420 | variant="contained" 421 | > 422 | {isSoldOut ? ( 423 | "STAKE" 424 | ) : isActive ? ( 425 | isMinting ? ( 426 | 427 | ) : ( 428 | "STAKE" 429 | ) 430 | ) : ( 431 | completed && setIsActive(true)} 434 | onComplete={() => setIsActive(true)} 435 | renderer={renderCounter} 436 | /> 437 | )} 438 | 439 |
440 | )} 441 |
442 | } 443 |
444 | 445 | setAlertState({ ...alertState, open: false })} 449 | > 450 | setAlertState({ ...alertState, open: false })} 452 | severity={alertState.severity} 453 | > 454 | {alertState.message} 455 | 456 | 457 |
458 | ); 459 | }; 460 | 461 | interface AlertState { 462 | open: boolean; 463 | message: string; 464 | severity: "success" | "info" | "warning" | "error" | undefined; 465 | } 466 | 467 | const renderCounter = ({ days, hours, minutes, seconds, completed }: any) => { 468 | return ( 469 | 470 | {hours + (days || 0) * 24} hours, {minutes} minutes, {seconds} seconds 471 | 472 | ); 473 | }; 474 | 475 | export default Home; 476 | -------------------------------------------------------------------------------- /rust/token-metadata/program/src/processor.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::MetadataError, 4 | instruction::MetadataInstruction, 5 | state::{ 6 | HeroData, 7 | // Key, MasterEditionV1, MasterEditionV2, MAX_MASTER_EDITION_LEN, 8 | PREFIX, 9 | // Metadata, EDITION, 10 | }, 11 | utils::{ 12 | // assert_data_valid, assert_signer, 13 | assert_owned_by, assert_initialized, 14 | // assert_mint_authority_matches_mint, assert_derivation, 15 | // assert_token_program_matches_package, 16 | // create_or_allocate_account_raw, get_owner_from_token_account, 17 | process_create_metadata_accounts_logic, 18 | process_purchase_hero_logic, 19 | // assert_update_authority_is_correct, 20 | // process_mint_new_edition_from_master_edition_via_token_logic, 21 | // transfer_mint_authority, 22 | CreateMetadataAccountsLogicArgs, 23 | PurchaseHeroLogicArgs, 24 | // puff_out_data_fields, 25 | // MintNewEditionFromMasterEditionViaTokenLogicArgs, 26 | }, 27 | }, 28 | // arrayref::array_ref, 29 | borsh::{BorshDeserialize, BorshSerialize}, 30 | solana_program::{ 31 | account_info::{next_account_info, AccountInfo}, 32 | entrypoint::ProgramResult, 33 | msg, 34 | // program_error::ProgramError, 35 | pubkey::Pubkey, 36 | }, 37 | spl_token::state::{Account, Mint}, 38 | // metaplex_token_vault::{error::VaultError, state::VaultState}, 39 | }; 40 | 41 | pub fn process_instruction<'a>( 42 | program_id: &'a Pubkey, 43 | accounts: &'a [AccountInfo<'a>], 44 | input: &[u8], 45 | ) -> ProgramResult { 46 | let instruction = MetadataInstruction::try_from_slice(input)?; 47 | match instruction { 48 | MetadataInstruction::CreateMetadataAccount(args) => { 49 | msg!("Instruction: Create Metadata Accounts"); 50 | process_create_metadata_accounts( 51 | program_id, 52 | accounts, 53 | args.data, 54 | args.id, 55 | ) 56 | } 57 | MetadataInstruction::UpdateHeroPrice(args) => { 58 | msg!("Instruction: Update Hero Price from Id"); 59 | process_update_hero_price( 60 | program_id, 61 | accounts, 62 | args.id, 63 | args.price, 64 | ) 65 | } 66 | MetadataInstruction::PurchaseHero(args) => { 67 | msg!("Instruction: Purchase Hero from Id"); 68 | process_purchase_hero( 69 | program_id, 70 | accounts, 71 | args.id, 72 | args.new_name, 73 | args.new_uri, 74 | args.new_price, 75 | ) 76 | } 77 | } 78 | } 79 | 80 | pub fn process_create_metadata_accounts<'a>( 81 | program_id: &'a Pubkey, 82 | accounts: &'a [AccountInfo<'a>], 83 | data: HeroData, 84 | id: u8, 85 | ) -> ProgramResult { 86 | let account_info_iter = &mut accounts.iter(); 87 | let metadata_account_info = next_account_info(account_info_iter)?; 88 | let payer_account_info = next_account_info(account_info_iter)?; 89 | let system_account_info = next_account_info(account_info_iter)?; 90 | let rent_info = next_account_info(account_info_iter)?; 91 | 92 | process_create_metadata_accounts_logic( 93 | &program_id, 94 | CreateMetadataAccountsLogicArgs { 95 | metadata_account_info, 96 | payer_account_info, 97 | system_account_info, 98 | rent_info, 99 | }, 100 | data, 101 | id, 102 | ) 103 | } 104 | 105 | /// Purchase hero from id instruction 106 | pub fn process_purchase_hero<'a>( 107 | program_id: &'a Pubkey, 108 | accounts: &'a [AccountInfo<'a>], 109 | id: u8, 110 | new_name: Option, 111 | new_uri: Option, 112 | price: Option, 113 | ) -> ProgramResult { 114 | let account_info_iter = &mut accounts.iter(); 115 | let herodata_account_info = next_account_info(account_info_iter)?; 116 | let payer_account_info = next_account_info(account_info_iter)?; 117 | let nft_owner_address_info = next_account_info(account_info_iter)?; 118 | let nft_account_info = next_account_info(account_info_iter)?; 119 | let new_token_mint_address = next_account_info(account_info_iter)?; 120 | let system_account_info = next_account_info(account_info_iter)?; 121 | let rent_info = next_account_info(account_info_iter)?; 122 | 123 | process_purchase_hero_logic( 124 | &program_id, 125 | PurchaseHeroLogicArgs { 126 | herodata_account_info, 127 | payer_account_info, 128 | nft_owner_address_info, 129 | nft_account_info, 130 | new_token_mint_address, 131 | system_account_info, 132 | rent_info, 133 | }, 134 | id, 135 | new_name, 136 | new_uri, 137 | price, 138 | ) 139 | } 140 | 141 | /// Update existing hero price instruction 142 | pub fn process_update_hero_price( 143 | program_id: &Pubkey, 144 | accounts: &[AccountInfo], 145 | hero_id: u8, 146 | new_price: u64, 147 | ) -> ProgramResult { 148 | let account_info_iter = &mut accounts.iter(); 149 | let metadata_account_info = next_account_info(account_info_iter)?; 150 | let owner_account_info = next_account_info(account_info_iter)?; 151 | let owner_nft_token_account_info = next_account_info(account_info_iter)?; 152 | let metadata_seeds = &[ 153 | PREFIX.as_bytes(), 154 | program_id.as_ref(), 155 | &[hero_id], 156 | ]; 157 | let (metadata_key, _) = 158 | Pubkey::find_program_address(metadata_seeds, program_id); 159 | if *metadata_account_info.key != metadata_key { 160 | msg!("----> Error: mismatch with hero id and parsed hero account"); 161 | return Err(MetadataError::InvalidMetadataKey.into()); 162 | } 163 | 164 | assert_owned_by(metadata_account_info, program_id)?; 165 | 166 | let mut metadata = HeroData::from_account_info(metadata_account_info)?; 167 | let token_account: Account = assert_initialized(&owner_nft_token_account_info)?; 168 | msg!("--> retrived: {}, generated: {}", metadata.owner_nft_address, token_account.mint); 169 | 170 | assert_owned_by(owner_nft_token_account_info, &spl_token::id())?; 171 | if metadata.owner_nft_address != token_account.mint { 172 | return Err(MetadataError::OwnerMismatch.into()); 173 | } 174 | msg!("---> Hero Onwer address: {}, Retrieved: {}", owner_account_info.key, token_account.owner); 175 | let token_account: Account = assert_initialized(&owner_nft_token_account_info)?; 176 | if token_account.owner != *owner_account_info.key { 177 | return Err(MetadataError::InvalidOwner.into()); 178 | } 179 | 180 | metadata.listed_price = new_price; 181 | 182 | metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; 183 | Ok(()) 184 | } 185 | 186 | // pub fn process_sign_metadata(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 187 | // let account_info_iter = &mut accounts.iter(); 188 | 189 | // let metadata_info = next_account_info(account_info_iter)?; 190 | // let creator_info = next_account_info(account_info_iter)?; 191 | 192 | // assert_signer(creator_info)?; 193 | // assert_owned_by(metadata_info, program_id)?; 194 | 195 | // let mut metadata = HeroData::from_account_info(metadata_info)?; 196 | 197 | // // if let Some(creators) = &mut metadata.data.creators { 198 | // // let mut found = false; 199 | // // for creator in creators { 200 | // // if creator.address == *creator_info.key { 201 | // // creator.verified = true; 202 | // // found = true; 203 | // // break; 204 | // // } 205 | // // } 206 | // // if !found { 207 | // // return Err(MetadataError::CreatorNotFound.into()); 208 | // // } 209 | // // } else { 210 | // // return Err(MetadataError::NoCreatorsPresentOnMetadata.into()); 211 | // // } 212 | // metadata.serialize(&mut *metadata_info.data.borrow_mut())?; 213 | 214 | // Ok(()) 215 | // } 216 | 217 | // / Puff out the variable length fields to a fixed length on a metadata 218 | // / account in a permissionless way. 219 | // pub fn process_puff_metadata_account( 220 | // program_id: &Pubkey, 221 | // accounts: &[AccountInfo], 222 | // ) -> ProgramResult { 223 | // let account_info_iter = &mut accounts.iter(); 224 | 225 | // let metadata_account_info = next_account_info(account_info_iter)?; 226 | // let mut metadata = HeroData::from_account_info(metadata_account_info)?; 227 | 228 | // assert_owned_by(metadata_account_info, program_id)?; 229 | 230 | // puff_out_data_fields(&mut metadata); 231 | 232 | // // let edition_seeds = &[ 233 | // // PREFIX.as_bytes(), 234 | // // program_id.as_ref(), 235 | // // metadata.mint.as_ref(), 236 | // // EDITION.as_bytes(), 237 | // // ]; 238 | // // let (_, edition_bump_seed) = Pubkey::find_program_address(edition_seeds, program_id); 239 | // // metadata.edition_nonce = Some(edition_bump_seed); 240 | 241 | // metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; 242 | // Ok(()) 243 | // } 244 | 245 | // pub fn process_update_primary_sale_happened_via_token( 246 | // program_id: &Pubkey, 247 | // accounts: &[AccountInfo], 248 | // ) -> ProgramResult { 249 | // let account_info_iter = &mut accounts.iter(); 250 | 251 | // let metadata_account_info = next_account_info(account_info_iter)?; 252 | // let owner_info = next_account_info(account_info_iter)?; 253 | // let token_account_info = next_account_info(account_info_iter)?; 254 | 255 | // let token_account: Account = assert_initialized(token_account_info)?; 256 | // let mut metadata = Metadata::from_account_info(metadata_account_info)?; 257 | 258 | // assert_owned_by(metadata_account_info, program_id)?; 259 | // assert_owned_by(token_account_info, &spl_token::id())?; 260 | 261 | // if !owner_info.is_signer { 262 | // return Err(ProgramError::MissingRequiredSignature); 263 | // } 264 | 265 | // if token_account.owner != *owner_info.key { 266 | // return Err(MetadataError::OwnerMismatch.into()); 267 | // } 268 | 269 | // if token_account.amount == 0 { 270 | // return Err(MetadataError::NoBalanceInAccountForAuthorization.into()); 271 | // } 272 | 273 | // if token_account.mint != metadata.mint { 274 | // return Err(MetadataError::MintMismatch.into()); 275 | // } 276 | 277 | // metadata.primary_sale_happened = true; 278 | // metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?; 279 | 280 | // Ok(()) 281 | // } 282 | 283 | // /// Create master edition 284 | // pub fn process_create_master_edition( 285 | // program_id: &Pubkey, 286 | // accounts: &[AccountInfo], 287 | // max_supply: Option, 288 | // ) -> ProgramResult { 289 | // let account_info_iter = &mut accounts.iter(); 290 | 291 | // let edition_account_info = next_account_info(account_info_iter)?; 292 | // let mint_info = next_account_info(account_info_iter)?; 293 | // let update_authority_info = next_account_info(account_info_iter)?; 294 | // let mint_authority_info = next_account_info(account_info_iter)?; 295 | // let payer_account_info = next_account_info(account_info_iter)?; 296 | // let metadata_account_info = next_account_info(account_info_iter)?; 297 | // let token_program_info = next_account_info(account_info_iter)?; 298 | // let system_account_info = next_account_info(account_info_iter)?; 299 | // let rent_info = next_account_info(account_info_iter)?; 300 | 301 | // let metadata = Metadata::from_account_info(metadata_account_info)?; 302 | // let mint: Mint = assert_initialized(mint_info)?; 303 | 304 | // let bump_seed = assert_derivation( 305 | // program_id, 306 | // edition_account_info, 307 | // &[ 308 | // PREFIX.as_bytes(), 309 | // program_id.as_ref(), 310 | // &mint_info.key.as_ref(), 311 | // EDITION.as_bytes(), 312 | // ], 313 | // )?; 314 | 315 | // assert_token_program_matches_package(token_program_info)?; 316 | // assert_mint_authority_matches_mint(&mint.mint_authority, mint_authority_info)?; 317 | // assert_owned_by(metadata_account_info, program_id)?; 318 | // assert_owned_by(mint_info, &spl_token::id())?; 319 | 320 | // if metadata.mint != *mint_info.key { 321 | // return Err(MetadataError::MintMismatch.into()); 322 | // } 323 | 324 | // if mint.decimals != 0 { 325 | // return Err(MetadataError::EditionMintDecimalsShouldBeZero.into()); 326 | // } 327 | 328 | // assert_update_authority_is_correct(&metadata, update_authority_info)?; 329 | 330 | // if mint.supply != 1 { 331 | // return Err(MetadataError::EditionsMustHaveExactlyOneToken.into()); 332 | // } 333 | 334 | // let edition_authority_seeds = &[ 335 | // PREFIX.as_bytes(), 336 | // program_id.as_ref(), 337 | // &mint_info.key.as_ref(), 338 | // EDITION.as_bytes(), 339 | // &[bump_seed], 340 | // ]; 341 | 342 | // create_or_allocate_account_raw( 343 | // *program_id, 344 | // edition_account_info, 345 | // rent_info, 346 | // system_account_info, 347 | // payer_account_info, 348 | // MAX_MASTER_EDITION_LEN, 349 | // edition_authority_seeds, 350 | // )?; 351 | 352 | // let mut edition = MasterEditionV2::from_account_info(edition_account_info)?; 353 | 354 | // edition.key = Key::MasterEditionV2; 355 | // edition.supply = 0; 356 | // edition.max_supply = max_supply; 357 | // edition.serialize(&mut *edition_account_info.data.borrow_mut())?; 358 | 359 | // // While you can't mint any more of your master record, you can 360 | // // mint as many limited editions as you like within your max supply. 361 | // transfer_mint_authority( 362 | // edition_account_info.key, 363 | // edition_account_info, 364 | // mint_info, 365 | // mint_authority_info, 366 | // token_program_info, 367 | // )?; 368 | 369 | // Ok(()) 370 | // } 371 | 372 | // pub fn process_mint_new_edition_from_master_edition_via_token<'a>( 373 | // program_id: &'a Pubkey, 374 | // accounts: &'a [AccountInfo<'a>], 375 | // edition: u64, 376 | // ignore_owner_signer: bool, 377 | // ) -> ProgramResult { 378 | // let account_info_iter = &mut accounts.iter(); 379 | 380 | // let new_metadata_account_info = next_account_info(account_info_iter)?; 381 | // let new_edition_account_info = next_account_info(account_info_iter)?; 382 | // let master_edition_account_info = next_account_info(account_info_iter)?; 383 | // let mint_info = next_account_info(account_info_iter)?; 384 | // let edition_marker_info = next_account_info(account_info_iter)?; 385 | // let mint_authority_info = next_account_info(account_info_iter)?; 386 | // let payer_account_info = next_account_info(account_info_iter)?; 387 | // let owner_account_info = next_account_info(account_info_iter)?; 388 | // let token_account_info = next_account_info(account_info_iter)?; 389 | // let update_authority_info = next_account_info(account_info_iter)?; 390 | // let master_metadata_account_info = next_account_info(account_info_iter)?; 391 | // let token_program_account_info = next_account_info(account_info_iter)?; 392 | // let system_account_info = next_account_info(account_info_iter)?; 393 | // let rent_info = next_account_info(account_info_iter)?; 394 | 395 | // process_mint_new_edition_from_master_edition_via_token_logic( 396 | // &program_id, 397 | // MintNewEditionFromMasterEditionViaTokenLogicArgs { 398 | // new_metadata_account_info, 399 | // new_edition_account_info, 400 | // master_edition_account_info, 401 | // mint_info, 402 | // edition_marker_info, 403 | // mint_authority_info, 404 | // payer_account_info, 405 | // owner_account_info, 406 | // token_account_info, 407 | // update_authority_info, 408 | // master_metadata_account_info, 409 | // token_program_account_info, 410 | // system_account_info, 411 | // rent_info, 412 | // }, 413 | // edition, 414 | // ignore_owner_signer, 415 | // ) 416 | // } 417 | 418 | // pub fn process_convert_master_edition_v1_to_v2( 419 | // program_id: &Pubkey, 420 | // accounts: &[AccountInfo], 421 | // ) -> ProgramResult { 422 | // let account_info_iter = &mut accounts.iter(); 423 | // let master_edition_info = next_account_info(account_info_iter)?; 424 | // let one_time_printing_auth_mint_info = next_account_info(account_info_iter)?; 425 | // let printing_mint_info = next_account_info(account_info_iter)?; 426 | 427 | // assert_owned_by(master_edition_info, program_id)?; 428 | // assert_owned_by(one_time_printing_auth_mint_info, &spl_token::id())?; 429 | // assert_owned_by(printing_mint_info, &spl_token::id())?; 430 | // let master_edition: MasterEditionV1 = MasterEditionV1::from_account_info(master_edition_info)?; 431 | // let printing_mint: Mint = assert_initialized(printing_mint_info)?; 432 | // let auth_mint: Mint = assert_initialized(one_time_printing_auth_mint_info)?; 433 | // if master_edition.one_time_printing_authorization_mint != *one_time_printing_auth_mint_info.key 434 | // { 435 | // return Err(MetadataError::OneTimePrintingAuthMintMismatch.into()); 436 | // } 437 | 438 | // if master_edition.printing_mint != *printing_mint_info.key { 439 | // return Err(MetadataError::PrintingMintMismatch.into()); 440 | // } 441 | 442 | // if printing_mint.supply != 0 { 443 | // return Err(MetadataError::PrintingMintSupplyMustBeZeroForConversion.into()); 444 | // } 445 | 446 | // if auth_mint.supply != 0 { 447 | // return Err(MetadataError::OneTimeAuthMintSupplyMustBeZeroForConversion.into()); 448 | // } 449 | 450 | // MasterEditionV2 { 451 | // key: Key::MasterEditionV2, 452 | // supply: master_edition.supply, 453 | // max_supply: master_edition.max_supply, 454 | // } 455 | // .serialize(&mut *master_edition_info.data.borrow_mut())?; 456 | 457 | // Ok(()) 458 | // } 459 | 460 | // pub fn process_mint_new_edition_from_master_edition_via_vault_proxy<'a>( 461 | // program_id: &'a Pubkey, 462 | // accounts: &'a [AccountInfo<'a>], 463 | // edition: u64, 464 | // ) -> ProgramResult { 465 | // let account_info_iter = &mut accounts.iter(); 466 | 467 | // let new_metadata_account_info = next_account_info(account_info_iter)?; 468 | // let new_edition_account_info = next_account_info(account_info_iter)?; 469 | // let master_edition_account_info = next_account_info(account_info_iter)?; 470 | // let mint_info = next_account_info(account_info_iter)?; 471 | // let edition_marker_info = next_account_info(account_info_iter)?; 472 | // let mint_authority_info = next_account_info(account_info_iter)?; 473 | // let payer_info = next_account_info(account_info_iter)?; 474 | // let vault_authority_info = next_account_info(account_info_iter)?; 475 | // let store_info = next_account_info(account_info_iter)?; 476 | // let safety_deposit_info = next_account_info(account_info_iter)?; 477 | // let vault_info = next_account_info(account_info_iter)?; 478 | // let update_authority_info = next_account_info(account_info_iter)?; 479 | // let master_metadata_account_info = next_account_info(account_info_iter)?; 480 | // let token_program_account_info = next_account_info(account_info_iter)?; 481 | // // we cant do much here to prove that this is the right token vault program except to prove that it matches 482 | // // the global one right now. We dont want to force people to use one vault program, 483 | // // so there is a bit of trust involved, but the attack vector here is someone provides 484 | // // an entirely fake vault program that claims to own token account X via it's pda but in order to spoof X's owner 485 | // // and get a free edition. However, we check that the owner of account X is the vault account's pda, so 486 | // // not sure how they would get away with it - they'd need to actually own that account! - J. 487 | // let token_vault_program_info = next_account_info(account_info_iter)?; 488 | // let system_account_info = next_account_info(account_info_iter)?; 489 | // let rent_info = next_account_info(account_info_iter)?; 490 | 491 | // let vault_data = vault_info.data.borrow(); 492 | // let safety_deposit_data = safety_deposit_info.data.borrow(); 493 | 494 | // // Since we're crunching out borsh for CPU units, do type checks this way 495 | // if vault_data[0] != metaplex_token_vault::state::Key::VaultV1 as u8 { 496 | // return Err(VaultError::DataTypeMismatch.into()); 497 | // } 498 | 499 | // if safety_deposit_data[0] != metaplex_token_vault::state::Key::SafetyDepositBoxV1 as u8 { 500 | // return Err(VaultError::DataTypeMismatch.into()); 501 | // } 502 | 503 | // // skip deserialization to keep things cheap on CPU 504 | // let token_program = Pubkey::new_from_array(*array_ref![vault_data, 1, 32]); 505 | // let vault_authority = Pubkey::new_from_array(*array_ref![vault_data, 65, 32]); 506 | // let store_on_sd = Pubkey::new_from_array(*array_ref![safety_deposit_data, 65, 32]); 507 | // let vault_on_sd = Pubkey::new_from_array(*array_ref![safety_deposit_data, 1, 32]); 508 | 509 | // let owner = get_owner_from_token_account(store_info)?; 510 | 511 | // let seeds = &[ 512 | // metaplex_token_vault::state::PREFIX.as_bytes(), 513 | // token_vault_program_info.key.as_ref(), 514 | // vault_info.key.as_ref(), 515 | // ]; 516 | // let (authority, _) = Pubkey::find_program_address(seeds, token_vault_program_info.key); 517 | 518 | // if owner != authority { 519 | // return Err(MetadataError::InvalidOwner.into()); 520 | // } 521 | 522 | // assert_signer(vault_authority_info)?; 523 | 524 | // // Since most checks happen next level down in token program, we only need to verify 525 | // // that the vault authority signer matches what's expected on vault to authorize 526 | // // use of our pda authority, and that the token store is right for the safety deposit. 527 | // // Then pass it through. 528 | // assert_owned_by(vault_info, token_vault_program_info.key)?; 529 | // assert_owned_by(safety_deposit_info, token_vault_program_info.key)?; 530 | // assert_owned_by(store_info, token_program_account_info.key)?; 531 | 532 | // if &token_program != token_program_account_info.key { 533 | // return Err(VaultError::TokenProgramProvidedDoesNotMatchVault.into()); 534 | // } 535 | 536 | // if !vault_authority_info.is_signer { 537 | // return Err(VaultError::AuthorityIsNotSigner.into()); 538 | // } 539 | // if *vault_authority_info.key != vault_authority { 540 | // return Err(VaultError::AuthorityDoesNotMatch.into()); 541 | // } 542 | 543 | // if vault_data[195] != VaultState::Combined as u8 { 544 | // return Err(VaultError::VaultShouldBeCombined.into()); 545 | // } 546 | 547 | // if vault_on_sd != *vault_info.key { 548 | // return Err(VaultError::SafetyDepositBoxVaultMismatch.into()); 549 | // } 550 | 551 | // if *store_info.key != store_on_sd { 552 | // return Err(VaultError::StoreDoesNotMatchSafetyDepositBox.into()); 553 | // } 554 | 555 | // let args = MintNewEditionFromMasterEditionViaTokenLogicArgs { 556 | // new_metadata_account_info, 557 | // new_edition_account_info, 558 | // master_edition_account_info, 559 | // mint_info, 560 | // edition_marker_info, 561 | // mint_authority_info, 562 | // payer_account_info: payer_info, 563 | // owner_account_info: vault_authority_info, 564 | // token_account_info: store_info, 565 | // update_authority_info, 566 | // master_metadata_account_info, 567 | // token_program_account_info, 568 | // system_account_info, 569 | // rent_info, 570 | // }; 571 | 572 | // process_mint_new_edition_from_master_edition_via_token_logic(program_id, args, edition, true) 573 | // } 574 | --------------------------------------------------------------------------------