├── .gitignore ├── img-generator ├── fonts │ ├── SF-Pro.ttf │ └── fonts.conf ├── assets │ ├── icons │ │ ├── infinity.png │ │ ├── expiration.png │ │ ├── extendable.png │ │ ├── returnable.png │ │ └── revocable.png │ ├── jambo │ │ ├── HUNTING.png │ │ └── TRAINING.png │ ├── namespaces │ │ ├── me.jpg │ │ ├── chat.jpg │ │ ├── discord.jpg │ │ ├── github.jpg │ │ ├── twitter.jpg │ │ ├── EmpireDAO.jpg │ │ ├── instagram.jpg │ │ ├── icons │ │ │ ├── discord.png │ │ │ └── twitter.png │ │ └── empiredao-registration.jpg │ ├── cardinal-crosshair.png │ └── staked-token-overlay │ │ └── POOl707.png ├── event-ticket-image.ts ├── staked-token.ts ├── handler.ts ├── namespace-image.ts ├── jambo-image.ts ├── img-utils.ts └── generator.ts ├── metadata-generator ├── event-ticket-metadata.ts ├── ticket.ts ├── twitter.ts ├── expired.ts ├── default.ts ├── handler.ts ├── attributes.ts └── generator.ts ├── .eslintrc.js ├── tsconfig.json ├── common ├── utils.ts ├── connection.ts ├── firebase.ts └── tokenData.ts ├── README.md ├── serverless.yml └── idls ├── LVLYTWmTaRCV5JcZ5HQkU1bhEjx34xqGiT3eWU6SuX9.json └── LVLYTWmTaRCV5JcZ5HQkU1bhEjx34xqGiT3eWU6SuX9.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .build 4 | .serverless 5 | -------------------------------------------------------------------------------- /img-generator/fonts/SF-Pro.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/fonts/SF-Pro.ttf -------------------------------------------------------------------------------- /img-generator/assets/icons/infinity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/icons/infinity.png -------------------------------------------------------------------------------- /img-generator/assets/jambo/HUNTING.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/jambo/HUNTING.png -------------------------------------------------------------------------------- /img-generator/assets/jambo/TRAINING.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/jambo/TRAINING.png -------------------------------------------------------------------------------- /img-generator/assets/namespaces/me.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/me.jpg -------------------------------------------------------------------------------- /img-generator/assets/icons/expiration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/icons/expiration.png -------------------------------------------------------------------------------- /img-generator/assets/icons/extendable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/icons/extendable.png -------------------------------------------------------------------------------- /img-generator/assets/icons/returnable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/icons/returnable.png -------------------------------------------------------------------------------- /img-generator/assets/icons/revocable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/icons/revocable.png -------------------------------------------------------------------------------- /img-generator/assets/namespaces/chat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/chat.jpg -------------------------------------------------------------------------------- /img-generator/assets/cardinal-crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/cardinal-crosshair.png -------------------------------------------------------------------------------- /img-generator/assets/namespaces/discord.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/discord.jpg -------------------------------------------------------------------------------- /img-generator/assets/namespaces/github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/github.jpg -------------------------------------------------------------------------------- /img-generator/assets/namespaces/twitter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/twitter.jpg -------------------------------------------------------------------------------- /img-generator/assets/namespaces/EmpireDAO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/EmpireDAO.jpg -------------------------------------------------------------------------------- /img-generator/assets/namespaces/instagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/instagram.jpg -------------------------------------------------------------------------------- /img-generator/assets/namespaces/icons/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/icons/discord.png -------------------------------------------------------------------------------- /img-generator/assets/namespaces/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/icons/twitter.png -------------------------------------------------------------------------------- /img-generator/assets/staked-token-overlay/POOl707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/staked-token-overlay/POOl707.png -------------------------------------------------------------------------------- /img-generator/assets/namespaces/empiredao-registration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roswelly/dynamic-nft-metadata/HEAD/img-generator/assets/namespaces/empiredao-registration.jpg -------------------------------------------------------------------------------- /img-generator/fonts/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /var/task/fonts 5 | /tmp/fonts-cache/ 6 | 7 | -------------------------------------------------------------------------------- /metadata-generator/event-ticket-metadata.ts: -------------------------------------------------------------------------------- 1 | export const getTicketMetadataLink = (ticketId: string) => { 2 | return `https://firebasestorage.googleapis.com/v0/b/solana-nft-programs-events.appspot.com/o/tickets%2F${ticketId}%2Fmetadata.json?alt=media`; 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | require("@rushstack/eslint-patch/modern-module-resolution"); 2 | 3 | module.exports = { 4 | root: true, 5 | ignorePatterns: [".build", "*.js"], 6 | parserOptions: { 7 | tsconfigRootDir: __dirname, 8 | project: "tsconfig.json", 9 | }, 10 | extends: ["@saberhq"], 11 | env: { 12 | node: true, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "preserveConstEnums": true, 4 | "strictNullChecks": true, 5 | "sourceMap": true, 6 | "allowJs": true, 7 | "target": "es5", 8 | "outDir": ".build", 9 | "moduleResolution": "node", 10 | "lib": ["es2015"], 11 | "rootDir": "./", 12 | "esModuleInterop": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /img-generator/event-ticket-image.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | 3 | import type { TokenData } from "../common/tokenData"; 4 | 5 | export const getTicketImageURL = (ticketId: string) => { 6 | return `https://firebasestorage.googleapis.com/v0/b/solana-nft-programs-events.appspot.com/o/tickets%2F${ticketId}%2Fimage.png?alt=media`; 7 | }; 8 | 9 | export async function getTicketImage(tokenData: TokenData): Promise { 10 | const mintName = tokenData?.metaplexData?.parsed.data.name; 11 | if (!mintName) throw "Mint name not found"; 12 | 13 | const imageURL = getTicketImageURL(mintName.toString()); 14 | const response = await fetch(imageURL); 15 | return Buffer.from(await response.arrayBuffer()); 16 | } 17 | -------------------------------------------------------------------------------- /common/utils.ts: -------------------------------------------------------------------------------- 1 | import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 2 | import type { Connection } from "@solana/web3.js"; 3 | import { Keypair, PublicKey } from "@solana/web3.js"; 4 | 5 | export const getOwner = async (connection: Connection, mintId: string) => { 6 | const mint = new PublicKey(mintId); 7 | const largestHolders = await connection.getTokenLargestAccounts(mint); 8 | const certificateMintToken = new Token( 9 | connection, 10 | mint, 11 | TOKEN_PROGRAM_ID, 12 | // not used 13 | Keypair.generate() 14 | ); 15 | 16 | const largestTokenAccount = 17 | largestHolders?.value[0]?.address && 18 | (await certificateMintToken.getAccountInfo( 19 | largestHolders?.value[0]?.address 20 | )); 21 | return largestTokenAccount.owner; 22 | }; 23 | -------------------------------------------------------------------------------- /metadata-generator/ticket.ts: -------------------------------------------------------------------------------- 1 | import type { Attribute } from "./attributes"; 2 | import type { NFTMetadata } from "./generator"; 3 | 4 | export const getDefaultTicketMetadata = ( 5 | mintId: string, 6 | cluster: string, 7 | attributes: Attribute[] 8 | ): NFTMetadata => { 9 | const imageUrl = `https://nft.host.so/img/${mintId}${ 10 | cluster ? `cluster=${cluster}` : "" 11 | }`; 12 | return { 13 | name: "Ticket", 14 | symbol: "TICKET", 15 | description: `This is a NFT representing your ticket for this event`, 16 | seller_fee_basis_points: 0, 17 | attributes: attributes, 18 | collection: { 19 | name: "Tickets", 20 | family: "Tickets", 21 | }, 22 | properties: { 23 | files: [ 24 | { 25 | uri: imageUrl, 26 | type: "image/png", 27 | }, 28 | ], 29 | category: "image", 30 | maxSupply: 1, 31 | creators: [], 32 | }, 33 | image: imageUrl, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /metadata-generator/twitter.ts: -------------------------------------------------------------------------------- 1 | import type { NFTMetadata } from "./generator"; 2 | 3 | export const getTwitterMetadata = ( 4 | fullName: string, 5 | mintId: string, 6 | ownerId: string, 7 | nameParam: string, 8 | cluster: string 9 | ): NFTMetadata => { 10 | const imageUrl = `https://nft.host.so/img/${mintId}?${ 11 | nameParam ? `&name=${encodeURIComponent(nameParam)}` : "" 12 | }${cluster ? `&cluster=${cluster}` : ""}`; 13 | return { 14 | name: fullName, 15 | symbol: "NAME", 16 | description: `This is a non-transferable NFT representing your ownership of ${fullName}`, 17 | seller_fee_basis_points: 0, 18 | external_url: `https://twitter.host.so/${ownerId}`, 19 | attributes: [], 20 | collection: { 21 | name: "Twitter", 22 | family: "Twitter", 23 | }, 24 | properties: { 25 | files: [ 26 | { 27 | uri: imageUrl, 28 | type: "image/png", 29 | }, 30 | ], 31 | category: "image", 32 | maxSupply: 1, 33 | creators: [], 34 | }, 35 | image: imageUrl, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /img-generator/staked-token.ts: -------------------------------------------------------------------------------- 1 | import * as canvas from "canvas"; 2 | 3 | import type { TokenData } from "../common/tokenData"; 4 | import { drawBackgroundImage, drawLogo } from "./img-utils"; 5 | 6 | export async function tryDrawStakedOverlay( 7 | tokenData: TokenData, 8 | imgUri?: string 9 | ) { 10 | const WIDTH = 250; 11 | const HEIGHT = 250; 12 | const imageCanvas = canvas.createCanvas(WIDTH, HEIGHT); 13 | if (tokenData?.metaplexData?.parsed.data.symbol && imgUri) { 14 | try { 15 | await drawBackgroundImage(imageCanvas, imgUri); 16 | await drawBackgroundImage( 17 | imageCanvas, 18 | __dirname.concat( 19 | `/assets/staked-token-overlay/${tokenData?.metaplexData?.parsed.data.symbol}.png` 20 | ), 21 | false 22 | ); 23 | await drawLogo( 24 | imageCanvas, 25 | 0.125 * imageCanvas.width, 26 | "bottom-left", 27 | 0.1 28 | ); 29 | return imageCanvas.toBuffer("image/png"); 30 | } catch (e) { 31 | console.log("Failed to staked overlay image: ", e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /metadata-generator/expired.ts: -------------------------------------------------------------------------------- 1 | import type { NFTMetadata } from "./generator"; 2 | 3 | const burnURL = (cluster: string) => { 4 | if (cluster === "devnet") { 5 | return "https://dev.host.so/burn"; 6 | } 7 | return "https://main.host.so/burn"; 8 | }; 9 | 10 | export const getExpiredMetadata = (cluster: string): NFTMetadata => { 11 | const imageUrl = `https://api.host.so/img/?text=EXPIRED`; 12 | return { 13 | name: "EXPIRED", 14 | symbol: "RCP", 15 | description: `This is a stale rental receipt from a past rental. Click the link here to burn it ${burnURL( 16 | cluster 17 | )}`, 18 | seller_fee_basis_points: 0, 19 | external_url: burnURL(cluster), 20 | attributes: [], 21 | collection: { 22 | name: "Expired Receipts", 23 | family: "Expired Receipts", 24 | }, 25 | properties: { 26 | files: [ 27 | { 28 | uri: imageUrl, 29 | type: "image/png", 30 | }, 31 | ], 32 | category: "image", 33 | maxSupply: 1, 34 | creators: [], 35 | }, 36 | image: imageUrl, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /metadata-generator/default.ts: -------------------------------------------------------------------------------- 1 | import { capitalizeFirstLetter } from "@solana-nft-programs/common"; 2 | 3 | import type { NFTMetadata } from "./generator"; 4 | 5 | export const getDefaultMetadata = ( 6 | namespace: string, 7 | fullName: string, 8 | mintId: string, 9 | nameParam: string, 10 | cluster: string 11 | ): NFTMetadata => { 12 | const imageUrl = `https://nft.host.so/img/${mintId}${ 13 | nameParam ? `?name=${encodeURIComponent(nameParam)}` : "" 14 | }${cluster ? `${nameParam ? "&" : "?"}cluster=${cluster}` : ""}`; 15 | return { 16 | name: fullName, 17 | symbol: "NAME", 18 | description: `This is a NFT representing your ${namespace} identity`, 19 | seller_fee_basis_points: 0, 20 | attributes: [], 21 | collection: { 22 | name: capitalizeFirstLetter(namespace), 23 | family: capitalizeFirstLetter(namespace), 24 | }, 25 | properties: { 26 | files: [ 27 | { 28 | uri: imageUrl, 29 | type: "image/png", 30 | }, 31 | ], 32 | category: "image", 33 | maxSupply: 1, 34 | creators: [], 35 | }, 36 | image: imageUrl, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /metadata-generator/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */ 2 | import { getMetadata } from "./generator"; 3 | 4 | module.exports.generate = async (event) => { 5 | const json = await getMetadata( 6 | event.pathParameters && event.pathParameters.mintId, 7 | event.queryStringParameters && event.queryStringParameters.name, 8 | event.queryStringParameters && event.queryStringParameters.uri, 9 | event.queryStringParameters && event.queryStringParameters.text, 10 | event.queryStringParameters && event.queryStringParameters.img, 11 | event.queryStringParameters && event.queryStringParameters.event, 12 | event.queryStringParameters && event.queryStringParameters.attrs, 13 | event.queryStringParameters && event.queryStringParameters.cluster 14 | ); 15 | const response = { 16 | statusCode: 200, 17 | headers: { 18 | "Access-Control-Allow-Methods": "*", 19 | "Access-Control-Allow-Origin": "*", // Required for CORS support to work 20 | "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS 21 | "content-type": "application/json", 22 | }, 23 | body: JSON.stringify(json), 24 | }; 25 | return response; 26 | }; 27 | -------------------------------------------------------------------------------- /img-generator/handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */ 2 | import { getImage } from "./generator"; 3 | 4 | module.exports.generate = async (event) => { 5 | const buffer = await getImage( 6 | event?.pathParameters?.mintId, 7 | event.queryStringParameters && event.queryStringParameters.name, 8 | event.queryStringParameters && event.queryStringParameters.uri, 9 | event.queryStringParameters && event.queryStringParameters.text, 10 | event.queryStringParameters && event.queryStringParameters.cluster 11 | ); 12 | 13 | console.log("Returning image buffer", buffer); 14 | const response = { 15 | statusCode: 200, 16 | headers: { 17 | "Access-Control-Allow-Methods": "*", 18 | "Access-Control-Allow-Origin": "*", // Required for CORS support to work 19 | "Access-Control-Allow-Credentials": true, // Required for cookies, authorization headers with HTTPS 20 | "content-type": "image/png", 21 | "content-disposition": `inline;filename="${ 22 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 23 | (event.pathParameters && event.pathParameters.mintId) || "untitled" 24 | }.png"`, 25 | }, 26 | body: buffer.toString("base64"), 27 | isBase64Encoded: true, 28 | }; 29 | return response; 30 | }; 31 | -------------------------------------------------------------------------------- /common/connection.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "@solana/web3.js"; 2 | 3 | const networkURLs: { [key: string]: { primary: string; secondary?: string } } = 4 | { 5 | ["mainnet-beta"]: { 6 | primary: 7 | process.env.MAINNET_PRIMARY || "https://solana-api.projectserum.com", 8 | secondary: 9 | process.env.MAINNET_SECONDARY || "https://solana-api.projectserum.com", 10 | }, 11 | mainnet: { 12 | primary: 13 | process.env.MAINNET_PRIMARY || "https://solana-api.projectserum.com", 14 | secondary: 15 | process.env.MAINNET_SECONDARY || "https://solana-api.projectserum.com", 16 | }, 17 | devnet: { primary: "https://api.devnet.solana.com/" }, 18 | testnet: { primary: "https://api.testnet.solana.com/" }, 19 | localnet: { primary: "http://localhost:8899/" }, 20 | }; 21 | 22 | export const connectionFor = ( 23 | cluster: string | null, 24 | defaultCluster = "mainnet" 25 | ) => { 26 | return new Connection( 27 | process.env.MAINNET_PRIMARY || 28 | networkURLs[cluster || defaultCluster].primary, 29 | "recent" 30 | ); 31 | }; 32 | 33 | export const secondaryConnectionFor = ( 34 | cluster: string | null, 35 | defaultCluster = "mainnet" 36 | ) => { 37 | return new Connection( 38 | process.env.RPC_URL || 39 | networkURLs[cluster || defaultCluster].secondary || 40 | networkURLs[cluster || defaultCluster].primary, 41 | "recent" 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic NFT Metadata Generator for Solana 2 | 3 | 4 | A powerful serverless API for generating dynamic NFT metadata and images on the Solana blockchain. This project provides a scalable solution for creating and managing NFT metadata with real-time updates and customization options. 5 | 6 | 7 | ## contacts 8 | - [telegram](https://t.me/caterpillardev) 9 | - [twitter](https://x.com/caterpillardev) 10 | 11 | 12 | 13 | ## Features 14 | 15 | - **Dynamic Metadata Generation**: Generate and update NFT metadata on-the-fly 16 | - **Image Generation**: Create custom NFT images with text and URI support 17 | - **Event Ticket Support**: Specialized metadata generation for event tickets 18 | - **Caching System**: Optimized performance with API Gateway caching 19 | - **Multi-Cluster Support**: Compatible with different Solana clusters 20 | - **TypeScript Support**: Built with type safety and modern development practices 21 | 22 | ## API Endpoints 23 | 24 | ### Image Generation 25 | 26 | - `GET /img/{mintId}`: Generate NFT image with custom parameters 27 | - `GET /img`: Base image generation endpoint 28 | 29 | ### Metadata Generation 30 | 31 | - `GET /metadata/{mintId}`: Generate NFT metadata with custom parameters 32 | - `GET /metadata`: Base metadata generation endpoint 33 | 34 | ## Configuration 35 | 36 | The project uses Serverless Framework for deployment and configuration. Key configuration files: 37 | 38 | - `serverless.yml`: Main configuration file 39 | - `tsconfig.json`: TypeScript configuration 40 | - `.eslintrc.js`: ESLint configuration 41 | 42 | ## Project Structure 43 | 44 | ``` 45 | api/ 46 | ├── img-generator/ # Image generation logic 47 | ├── metadata-generator/# Metadata generation logic 48 | ├── common/ # Shared utilities 49 | ├── idls/ # Interface definitions 50 | └── serverless.yml # Serverless configuration 51 | ``` 52 | 53 | ## Security 54 | 55 | - API Gateway caching with configurable TTL 56 | - Environment variable management 57 | - CORS support 58 | - Binary media type handling 59 | -------------------------------------------------------------------------------- /img-generator/namespace-image.ts: -------------------------------------------------------------------------------- 1 | import { breakIdentity, formatName } from "@solana-nft-programs/namespaces"; 2 | import * as canvas from "canvas"; 3 | 4 | import type { TokenData } from "../common/tokenData"; 5 | import { 6 | drawBackgroundImage, 7 | drawDefaultBackground, 8 | drawText, 9 | } from "./img-utils"; 10 | 11 | const WIDTH = 500; 12 | const HEIGHT = 500; 13 | 14 | const IGNORE_TEXT = ["empiredao-registration", "EmpireDAO"]; 15 | 16 | export async function getNamespaceImage( 17 | tokenData: TokenData, 18 | nameParam: string | undefined, 19 | textParam: string | undefined 20 | ): Promise { 21 | const mintName = tokenData?.metaplexData?.parsed.data.name; 22 | const namespace = 23 | (nameParam && !tokenData?.metaplexData?.parsed.data.name?.includes(".") 24 | ? tokenData?.metaplexData?.parsed.data.name 25 | : breakIdentity(mintName || textParam || "")[0]) || ""; 26 | const entryName = nameParam 27 | ? nameParam 28 | : breakIdentity(mintName || textParam || "")[1]; 29 | 30 | console.log(`Drawing namespace image for [${entryName}, ${namespace}]`); 31 | const imageCanvas = canvas.createCanvas(WIDTH, HEIGHT); 32 | 33 | try { 34 | await drawBackgroundImage( 35 | imageCanvas, 36 | __dirname.concat(`/assets/namespaces/${namespace}.jpg`) 37 | ); 38 | } catch (e) { 39 | console.log("Failed to draw background image: ", e); 40 | drawDefaultBackground(imageCanvas); 41 | } 42 | 43 | let topRightText: string | undefined; 44 | let nameText = decodeURIComponent(formatName(namespace, entryName)); 45 | if (namespace === "discord") { 46 | const temp = nameText.split("#"); 47 | nameText = temp.slice(0, -1).join(); 48 | topRightText = temp.pop(); 49 | } 50 | 51 | if (!IGNORE_TEXT.includes(namespace)) { 52 | const name = decodeURIComponent(formatName(namespace, entryName)).split( 53 | "#" 54 | )[0]; 55 | drawText(imageCanvas, name); 56 | } 57 | 58 | if (topRightText) { 59 | const topRightCtx = imageCanvas.getContext("2d"); 60 | topRightCtx.font = `${0.08 * WIDTH}px SFPro`; 61 | topRightCtx.fillStyle = "white"; 62 | topRightCtx.textAlign = "right"; 63 | topRightCtx.fillText("#" + topRightText, WIDTH * 0.95, HEIGHT * 0.1); 64 | } 65 | return imageCanvas.toBuffer("image/png"); 66 | } 67 | -------------------------------------------------------------------------------- /metadata-generator/attributes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InvalidationType, 3 | TokenManagerState, 4 | } from "@solana-nft-programs/token-manager/dist/cjs/programs/tokenManager"; 5 | 6 | import type { TokenData } from "../common/tokenData"; 7 | 8 | export type Attribute = { 9 | trait_type: string; 10 | value: string; 11 | display_type: string; 12 | }; 13 | 14 | export const stateAttributes = (tokenData: TokenData) => { 15 | if (tokenData?.tokenManagerData?.parsed.state) { 16 | return [ 17 | { 18 | trait_type: "state", 19 | value: 20 | tokenData?.tokenManagerData?.parsed.state === 21 | TokenManagerState.Invalidated 22 | ? "INVALIDATED" 23 | : "VALID", 24 | display_type: "State", 25 | }, 26 | ]; 27 | } 28 | return []; 29 | }; 30 | 31 | export const typeAttributes = (tokenData: TokenData) => { 32 | if ( 33 | tokenData.tokenManagerData?.parsed.invalidationType === 34 | InvalidationType.Return 35 | ) { 36 | return [ 37 | { 38 | trait_type: "type", 39 | value: "Rental", 40 | display_type: "Type", 41 | }, 42 | ]; 43 | } else if ( 44 | tokenData.tokenManagerData?.parsed.invalidationType === 45 | InvalidationType.Release 46 | ) { 47 | return [ 48 | { 49 | trait_type: "type", 50 | value: "Release", 51 | display_type: "Type", 52 | }, 53 | ]; 54 | } 55 | return []; 56 | }; 57 | 58 | export const usageAttributes = (tokenData: TokenData) => { 59 | if (tokenData.useInvalidatorData?.parsed.usages) { 60 | return [ 61 | { 62 | trait_type: "used", 63 | value: `(${tokenData.useInvalidatorData?.parsed.usages.toNumber()}${ 64 | tokenData.useInvalidatorData?.parsed.maxUsages 65 | ? `/${tokenData.useInvalidatorData?.parsed.maxUsages.toNumber()}` 66 | : "" 67 | })`, 68 | display_type: "Used", 69 | }, 70 | ]; 71 | } 72 | return []; 73 | }; 74 | 75 | export const expirationAttributes = (tokenData: TokenData) => { 76 | const expiration = 77 | tokenData.timeInvalidatorData?.parsed.maxExpiration || 78 | tokenData.timeInvalidatorData?.parsed.expiration; 79 | if (expiration) { 80 | return [ 81 | { 82 | trait_type: "expiration", 83 | value: `${new Date(expiration.toNumber() * 1000).toLocaleTimeString( 84 | "en-US", 85 | { 86 | month: "numeric", 87 | day: "numeric", 88 | year: "numeric", 89 | hour: "numeric", 90 | minute: "numeric", 91 | timeZone: "America/New_York", 92 | timeZoneName: "short", 93 | } 94 | )}`, 95 | display_type: "Expiration", 96 | }, 97 | ]; 98 | } 99 | return []; 100 | }; 101 | export const durationAttributes = (tokenData: TokenData) => { 102 | const duration = tokenData.timeInvalidatorData?.parsed?.durationSeconds; 103 | if (duration) { 104 | return [ 105 | { 106 | trait_type: "duration", 107 | value: `${duration.toNumber()}`, 108 | display_type: "Duration", 109 | }, 110 | ]; 111 | } 112 | return []; 113 | }; 114 | -------------------------------------------------------------------------------- /common/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import type { DocumentReference } from "firebase/firestore"; 3 | import { doc, getDoc, getFirestore } from "firebase/firestore"; 4 | import { getStorage } from "firebase/storage"; 5 | 6 | const firebaseConfig = { 7 | apiKey: "AIzaSyCJgPBVSp2TokeX_UpydLf4M7yamYA0nhs", 8 | authDomain: "solana-nft-programs-events.firebaseapp.com", 9 | projectId: "solana-nft-programs-events", 10 | storageBucket: "solana-nft-programs-events.appspot.com", 11 | messagingSenderId: "453139651235", 12 | appId: "1:453139651235:web:67443d5b218b600e7f3d16", 13 | measurementId: "G-R9SVMD5CRT", 14 | }; 15 | 16 | export const firebaseEventApp = initializeApp(firebaseConfig); 17 | export const eventFirestore = getFirestore(firebaseEventApp); 18 | export const eventStorage = getStorage(firebaseEventApp); 19 | 20 | export const getTicketRef = ( 21 | eventDocumentId?: string, 22 | ticketDocumentId?: string 23 | ): DocumentReference => { 24 | if (ticketDocumentId) { 25 | return doc(eventFirestore, "tickets", ticketDocumentId); 26 | } 27 | 28 | if (!eventDocumentId) { 29 | throw "No event id passed in"; 30 | } 31 | const generatedTicketId = `crd-${eventDocumentId}-${ 32 | Math.floor(Math.random() * 90000) + 10000 33 | }`; 34 | return doc(eventFirestore, "tickets", generatedTicketId); 35 | }; 36 | 37 | export const tryGetEventTicket = async ( 38 | ticketDocId: string 39 | ): Promise => { 40 | const ticketRef = getTicketRef(undefined, ticketDocId); 41 | const ticketSnap = await getDoc(ticketRef); 42 | if (ticketSnap.exists()) { 43 | return ticketSnap.data() as FirebaseTicket; 44 | } else { 45 | return undefined; 46 | } 47 | }; 48 | 49 | export const tryGetEvent = async ( 50 | docId: string 51 | ): Promise => { 52 | const eventRef = doc(eventFirestore, "events", docId); 53 | const eventsSnap = await getDoc(eventRef); 54 | if (!eventsSnap.exists()) { 55 | return undefined; 56 | } 57 | return eventsSnap.data() as FirebaseEvent; 58 | }; 59 | 60 | 61 | export type FirebaseEvent = { 62 | bannerImage: string | null; 63 | config: string | null; 64 | creatorAddress: string; 65 | description: string; 66 | docId: string; 67 | endTime: string; 68 | environment: string; 69 | location: string; 70 | name: string; 71 | paymentMint: string; 72 | shortLink: string; 73 | startTime: string; 74 | questions: string[]; 75 | timezone?: string; 76 | }; 77 | 78 | export type FirebaseTicket = { 79 | additionalSigners: string[] | null; 80 | description: string | null; 81 | docId: string; 82 | eventId: string; 83 | feePayer: string | null; 84 | name: string; 85 | price: number; 86 | paymentMint: string | null; 87 | quantity: number; 88 | ticketSignerAddress: string; 89 | ticketSignerId: string; 90 | totalClaimed: number; 91 | 92 | allowedCollections: string[] | null; 93 | allowedVerifiedCreators: string[] | null; 94 | allowedMints: string[] | null; 95 | 96 | verifiedTokenMaximum: number | null; 97 | approverAddressMaximum: number | null; 98 | approverEmailMaximum: number | null; 99 | claimerAddressMaximum: number | null; 100 | 101 | includeQRCode: boolean; 102 | }; 103 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | org: jpbogle 2 | app: solana-nft-programs-generator 3 | service: solana-nft-programs-generator 4 | frameworkVersion: "2 || 3" 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | versionFunctions: false 10 | lambdaHashingVersion: "20201221" 11 | environment: 12 | MAINNET_PRIMARY: ${param:MAINNET_PRIMARY} 13 | MAINNET_SECONDARY: ${param:MAINNET_SECONDARY} 14 | http: 15 | cors: true 16 | apiGateway: 17 | binaryMediaTypes: 18 | - "*/*" 19 | 20 | package: 21 | individually: true 22 | include: 23 | - ./idls/** 24 | - ./img-generator/fonts/** 25 | - ./img-generator/assets/** 26 | exclude: 27 | - "./node_modules" 28 | - "./package-lock.json" 29 | - "./yarn.lock" 30 | 31 | functions: 32 | img-generator-base: 33 | environment: 34 | FONTCONFIG_FILE: /var/task/fonts/fonts.conf 35 | handler: img-generator/handler.generate 36 | timeout: 10 37 | events: 38 | - http: 39 | path: /img 40 | method: get 41 | contentHandling: CONVERT_TO_BINARY 42 | caching: 43 | enabled: true 44 | ttlInSeconds: 10 45 | perKeyInvalidation: 46 | requireAuthorization: false 47 | cacheKeyParameters: 48 | - name: request.querystring.name 49 | - name: request.querystring.uri 50 | - name: request.querystring.text 51 | - name: request.querystring.cluster 52 | img-generator: 53 | environment: 54 | FONTCONFIG_FILE: /var/task/fonts/fonts.conf 55 | handler: img-generator/handler.generate 56 | timeout: 10 57 | events: 58 | - http: 59 | path: /img/{mintId} 60 | method: get 61 | contentHandling: CONVERT_TO_BINARY 62 | caching: 63 | enabled: true 64 | ttlInSeconds: 10 65 | perKeyInvalidation: 66 | requireAuthorization: false 67 | cacheKeyParameters: 68 | - name: request.path.mintId 69 | - name: request.querystring.name 70 | - name: request.querystring.uri 71 | - name: request.querystring.text 72 | - name: request.querystring.cluster 73 | metadata-generator-base: 74 | handler: metadata-generator/handler.generate 75 | environment: 76 | timeout: 10 77 | events: 78 | - http: 79 | path: /metadata 80 | method: get 81 | caching: 82 | enabled: true 83 | ttlInSeconds: 10 84 | perKeyInvalidation: 85 | requireAuthorization: false 86 | cacheKeyParameters: 87 | - name: request.querystring.name 88 | - name: request.querystring.uri 89 | - name: request.querystring.text 90 | - name: request.querystring.img 91 | - name: request.querystring.event 92 | - name: request.querystring.attrs 93 | - name: request.querystring.cluster 94 | metadata-generator: 95 | handler: metadata-generator/handler.generate 96 | environment: 97 | timeout: 10 98 | events: 99 | - http: 100 | path: /metadata/{mintId} 101 | method: get 102 | caching: 103 | enabled: true 104 | ttlInSeconds: 10 105 | perKeyInvalidation: 106 | requireAuthorization: false 107 | cacheKeyParameters: 108 | - name: request.path.mintId 109 | - name: request.querystring.name 110 | - name: request.querystring.uri 111 | - name: request.querystring.text 112 | - name: request.querystring.img 113 | - name: request.querystring.event 114 | - name: request.querystring.attrs 115 | - name: request.querystring.cluster 116 | 117 | custom: 118 | apigwBinary: 119 | types: 120 | - "image/png" 121 | apiGatewayCaching: 122 | enabled: true 123 | domains: 124 | main: 125 | domainName: api.host.so 126 | dev: 127 | domainName: dev-api.host.so 128 | # customDomain: 129 | # domainName: ${self:custom.domains.${opt:stage}.domainName} 130 | # certificateName: "*.host.so" 131 | # createRoute53Record: true 132 | # autoDomain: true 133 | 134 | plugins: 135 | # - serverless-domain-manager 136 | - serverless-apigw-binary 137 | - serverless-apigwy-binary 138 | - serverless-plugin-typescript 139 | - serverless-offline 140 | - serverless-api-gateway-caching 141 | - serverless-plugin-include-dependencies 142 | - serverless-prune-plugin 143 | -------------------------------------------------------------------------------- /img-generator/jambo-image.ts: -------------------------------------------------------------------------------- 1 | import * as questPool from "@solana-nft-programs/quest-pool"; 2 | import * as stakePool from "@solana-nft-programs/stake-pool"; 3 | import { getLevelNumber } from "@solana-nft-programs/stake-pool"; 4 | import { BN } from "@project-serum/anchor"; 5 | import * as web3 from "@solana/web3.js"; 6 | import * as canvas from "canvas"; 7 | 8 | import type { TokenData } from "../common/tokenData"; 9 | import { getTokenData } from "../common/tokenData"; 10 | 11 | export async function getJamboImage( 12 | tokenData: TokenData, 13 | connection: web3.Connection, 14 | textParam?: string, 15 | imgUri?: string 16 | ) { 17 | console.log("Rendering jambo image"); 18 | const originalMint = tokenData?.certificateData?.parsed 19 | .originalMint as web3.PublicKey; 20 | let originalTokenData: TokenData | null = null; 21 | 22 | // ovverride uri with originalMint uri if present 23 | if (originalMint) { 24 | try { 25 | originalTokenData = await getTokenData(connection, originalMint, true); 26 | } catch (e) { 27 | console.log( 28 | `Error fetching metaplex metadata for original mint (${originalMint.toString()})`, 29 | e 30 | ); 31 | } 32 | } 33 | 34 | canvas.registerFont(__dirname.concat("/fonts/SF-Pro.ttf"), { 35 | family: "SFPro", 36 | }); 37 | const WIDTH = 250; 38 | const HEIGHT = 250; 39 | const imageCanvas = canvas.createCanvas(WIDTH, HEIGHT); 40 | const GROUP_AND_HUNGRY_THRESHOLD = 10000; 41 | 42 | // draw base image 43 | const baseImgUri = originalTokenData?.metadata?.data.image || imgUri; 44 | if (baseImgUri) { 45 | const backgroundCtx = imageCanvas.getContext("2d"); 46 | backgroundCtx.fillStyle = "rgba(26, 27, 32, 1)"; 47 | backgroundCtx.fillRect(0, 0, WIDTH, HEIGHT); 48 | 49 | const img = await canvas.loadImage(baseImgUri); 50 | const imgContext = imageCanvas.getContext("2d"); 51 | if (img.height > img.width) { 52 | const imgHeightMultiplier = WIDTH / img.height; 53 | imgContext.drawImage( 54 | img, 55 | (WIDTH - img.width * imgHeightMultiplier) / 2, 56 | 0, 57 | img.width * imgHeightMultiplier, 58 | HEIGHT 59 | ); 60 | } else { 61 | const imgWidthMultiplier = HEIGHT / img.width; 62 | imgContext.drawImage( 63 | img, 64 | 0, 65 | (HEIGHT - img.height * imgWidthMultiplier) / 2, 66 | WIDTH, 67 | img.height * imgWidthMultiplier 68 | ); 69 | } 70 | } 71 | // overlay 72 | const overlayCtx = imageCanvas.getContext("2d"); 73 | overlayCtx.fillStyle = "rgba(26, 27, 32, 0.3)"; 74 | overlayCtx.fillRect(0, 0, WIDTH, HEIGHT); 75 | // // logo 76 | // const logoCtx = imageCanvas.getContext("2d"); 77 | // logoCtx.drawImage( 78 | // logo, 79 | // HEIGHT - PADDING / 1.5 - HEIGHT * 0.16, 80 | // WIDTH - PADDING / 1.5 - WIDTH * 0.16, 81 | // WIDTH * 0.16, 82 | // HEIGHT * 0.16 83 | // ); 84 | 85 | // date 86 | const dateCtx = imageCanvas.getContext("2d"); 87 | const UTCNow = Date.now() / 1000; 88 | dateCtx.font = "500 15px SFPro"; 89 | dateCtx.fillStyle = "white"; 90 | if (textParam === "TRAINING") { 91 | const entry = await stakePool.getStakeEntry(connection, originalMint); 92 | const lastStakedAt = entry?.parsed.lastStakedAt.toNumber() || UTCNow; 93 | const stakeBoost = (entry?.parsed.stakeBoost || new BN(1)).toNumber(); 94 | const totalStakeSeconds = ( 95 | entry?.parsed.totalStakeSeconds || new BN(0) 96 | ).toNumber(); 97 | const stakedTime = 98 | totalStakeSeconds + 99 | (stakeBoost / (stakeBoost >= GROUP_AND_HUNGRY_THRESHOLD ? 10000 : 100)) * 100 | (UTCNow - lastStakedAt); 101 | 102 | const [level, requiredSeconds] = getLevelNumber(stakedTime); 103 | const levelUpDate = new Date( 104 | (UTCNow + (requiredSeconds - stakedTime)) * 1000 105 | ); 106 | const date = levelUpDate.toLocaleDateString().split("/"); 107 | const time = levelUpDate.toTimeString().split(":"); 108 | dateCtx.fillText( 109 | `Level ${level + 1} on ${date[0]}/${date[1]}/${date[2].substring( 110 | date[2].length - 2, 111 | date[2].length 112 | )} ${time[0]}:${time[1]} GMT`, 113 | 30, 114 | 20 115 | ); 116 | } else { 117 | const [questEntryId] = await questPool.findQuestEntryId( 118 | new web3.PublicKey(originalMint) 119 | ); 120 | const entry = ( 121 | await questPool.getQuestEntries(connection, [questEntryId]) 122 | )[0]; 123 | try { 124 | const pool = ( 125 | await questPool.getQuestPools(connection, [ 126 | new web3.PublicKey(entry.parsed.stakePool.toString()), 127 | ]) 128 | )[0]; 129 | const questStart = entry?.parsed.questStart.toNumber() || UTCNow; 130 | const questDuration = pool.parsed.rewardDurationSeconds.toNumber(); 131 | const questEnd = new Date( 132 | (UTCNow + (questDuration - (UTCNow - questStart))) * 1000 133 | ); 134 | const date = questEnd.toLocaleDateString().split("/"); 135 | const time = questEnd.toTimeString().split(":"); 136 | dateCtx.fillText( 137 | `Claimable on ${date[0]}/${date[1]}/${date[2].substring( 138 | date[2].length - 2, 139 | date[2].length 140 | )} ${time[0]}:${time[1]} GMT`, 141 | 20, 142 | 20 143 | ); 144 | } catch (e) { 145 | console.log(e); 146 | } 147 | } 148 | 149 | const nameCtx = imageCanvas.getContext("2d"); 150 | const textImageUrl = textParam === "TRAINING" ? "TRAINING" : "HUNTING"; 151 | const textImage = await canvas.loadImage( 152 | __dirname.concat(`/assets/jambo/${textImageUrl}.png`) 153 | ); 154 | nameCtx.drawImage( 155 | textImage, 156 | WIDTH * 0.1, 157 | HEIGHT * 0.4, 158 | WIDTH * 0.8, 159 | WIDTH * 0.2 160 | ); 161 | 162 | return imageCanvas.toBuffer("image/png"); 163 | } 164 | -------------------------------------------------------------------------------- /img-generator/img-utils.ts: -------------------------------------------------------------------------------- 1 | import { TokenManagerState } from "@solana-nft-programs/token-manager/dist/cjs/programs/tokenManager"; 2 | import * as canvas from "canvas"; 3 | 4 | const COLOR_RED = "rgba(200, 0, 0, 1)"; 5 | const COLOR_ORANGE = "rgba(89, 56, 21, 1)"; 6 | const COLOR_GREEN = "rgba(39, 73, 22, 1)"; 7 | 8 | const textStyles = ["none", "overlay", "header"] as const; 9 | type TextStyleOptions = { 10 | defaultStyle?: typeof textStyles[number]; 11 | backgroundColor?: string; 12 | fillStyles?: string; 13 | }; 14 | 15 | const getStyleAndText = ( 16 | textParam: string, 17 | defaultStyle?: typeof textStyles[number] 18 | ) => { 19 | const match = textStyles.find((style) => style === textParam.split(":")[0]); 20 | if (match) { 21 | return [match, textParam.split(":")[1]]; 22 | } 23 | return [defaultStyle || "none", textParam]; 24 | }; 25 | 26 | export const drawText = ( 27 | imageCanvas: canvas.Canvas, 28 | textParam: string, 29 | styleOptions?: TextStyleOptions 30 | ) => { 31 | canvas.registerFont(__dirname.concat("/fonts/SF-Pro.ttf"), { 32 | family: "SFPro", 33 | }); 34 | const nameCtx = imageCanvas.getContext("2d"); 35 | const [style, text] = getStyleAndText(textParam, styleOptions?.defaultStyle); 36 | switch (style) { 37 | case "overlay": 38 | nameCtx.fillStyle = "rgba(180, 180, 180, 0.6)"; 39 | nameCtx.fillRect( 40 | 0.15 * imageCanvas.width, 41 | imageCanvas.height * 0.5 - 0.075 * imageCanvas.height, 42 | 0.7 * imageCanvas.width, 43 | 0.15 * imageCanvas.height 44 | ); 45 | nameCtx.strokeStyle = "rgba(255, 255, 255, 1)"; 46 | nameCtx.lineWidth = 0.015 * imageCanvas.width; 47 | nameCtx.strokeRect( 48 | 0.15 * imageCanvas.width, 49 | imageCanvas.height * 0.5 - 0.075 * imageCanvas.height, 50 | 0.7 * imageCanvas.width, 51 | 0.15 * imageCanvas.height 52 | ); 53 | 54 | nameCtx.font = `${0.075 * imageCanvas.width}px SFPro`; 55 | nameCtx.fillStyle = "white"; 56 | nameCtx.textAlign = "center"; 57 | nameCtx.textBaseline = "middle"; 58 | nameCtx.fillText(text, imageCanvas.width * 0.5, imageCanvas.height * 0.5); 59 | return; 60 | case "header": 61 | nameCtx.fillStyle = styleOptions?.backgroundColor ?? "rgba(0, 0, 0, 0)"; 62 | nameCtx.fillRect(0, 0, imageCanvas.width, 0.2 * imageCanvas.height); 63 | nameCtx.font = `${0.075 * imageCanvas.width}px SFPro`; 64 | nameCtx.fillStyle = styleOptions?.fillStyles ?? "white"; 65 | nameCtx.textAlign = "center"; 66 | nameCtx.textBaseline = "middle"; 67 | nameCtx.fillText(text, imageCanvas.width * 0.5, imageCanvas.width * 0.15); 68 | return; 69 | case "topRight": 70 | nameCtx.font = `${0.1 * imageCanvas.width}px SFPro`; 71 | nameCtx.fillStyle = "white"; 72 | nameCtx.textAlign = "right"; 73 | nameCtx.fillText( 74 | "#" + text, 75 | imageCanvas.width * 0.95, 76 | imageCanvas.height * 0.1 77 | ); 78 | return; 79 | default: 80 | nameCtx.font = `${0.1 * imageCanvas.width}px SFPro`; 81 | nameCtx.fillStyle = "white"; 82 | nameCtx.textAlign = "center"; 83 | nameCtx.textBaseline = "middle"; 84 | nameCtx.fillText(text, imageCanvas.width * 0.5, imageCanvas.height * 0.5); 85 | return; 86 | } 87 | }; 88 | 89 | export const drawShadow = ( 90 | imageCanvas: canvas.Canvas, 91 | tokenManagerState: TokenManagerState 92 | ) => { 93 | const shadowCtx = imageCanvas.getContext("2d"); 94 | 95 | switch (tokenManagerState) { 96 | case TokenManagerState.Issued: 97 | shadowCtx.shadowColor = COLOR_ORANGE; 98 | break; 99 | case TokenManagerState.Invalidated: 100 | shadowCtx.shadowColor = COLOR_RED; 101 | break; 102 | default: 103 | shadowCtx.shadowColor = COLOR_GREEN; 104 | } 105 | 106 | shadowCtx.shadowBlur = 0.04 * imageCanvas.width; 107 | shadowCtx.lineWidth = 0.04 * imageCanvas.width; 108 | shadowCtx.strokeStyle = "rgba(26, 27, 32, 0)"; 109 | shadowCtx.strokeRect(0, 0, imageCanvas.width, imageCanvas.height); 110 | shadowCtx.shadowBlur = 0; 111 | }; 112 | 113 | export const drawLogo = async ( 114 | imageCanvas: canvas.Canvas, 115 | paddingOverride?: number, 116 | location?: "bottom-right" | "bottom-left", 117 | pctSize = 0.16 118 | ) => { 119 | const padding = paddingOverride ?? 0.05 * imageCanvas.width; 120 | const logoCtx = imageCanvas.getContext("2d"); 121 | const logo = await canvas.loadImage( 122 | __dirname.concat("/assets/solana-nft-programs-crosshair.png") 123 | ); 124 | logoCtx.drawImage( 125 | logo, 126 | location === "bottom-left" 127 | ? padding / 1.5 128 | : imageCanvas.width - padding / 1.5 - imageCanvas.width * pctSize, 129 | imageCanvas.height - padding / 1.5 - imageCanvas.height * pctSize, 130 | imageCanvas.width * pctSize, 131 | imageCanvas.height * pctSize 132 | ); 133 | }; 134 | 135 | export const drawDefaultBackground = (imageCanvas: canvas.Canvas) => { 136 | const backgroundCtx = imageCanvas.getContext("2d"); 137 | const maxWidth = 138 | Math.sqrt( 139 | imageCanvas.width * imageCanvas.width + 140 | imageCanvas.height * imageCanvas.height 141 | ) / 2; 142 | const angle = 0.45; 143 | const grd = backgroundCtx.createLinearGradient( 144 | imageCanvas.width / 2 + Math.cos(angle) * maxWidth, // start pos 145 | imageCanvas.height / 2 + Math.sin(angle) * maxWidth, 146 | imageCanvas.width / 2 - Math.cos(angle) * maxWidth, // end pos 147 | imageCanvas.height / 2 - Math.sin(angle) * maxWidth 148 | ); 149 | grd.addColorStop(0, "#4C1734"); 150 | grd.addColorStop(1, "#000"); 151 | backgroundCtx.fillStyle = grd; 152 | backgroundCtx.fillRect(0, 0, imageCanvas.width, imageCanvas.height); 153 | }; 154 | 155 | export const drawBackgroundImage = async ( 156 | imageCanvas: canvas.Canvas, 157 | imageUrl: string, 158 | fill = true 159 | ) => { 160 | const imgBackgroundCtx = imageCanvas.getContext("2d"); 161 | 162 | if (fill) { 163 | imgBackgroundCtx.fillStyle = "rgba(26, 27, 32, 1)"; 164 | imgBackgroundCtx.fillRect(0, 0, imageCanvas.width, imageCanvas.height); 165 | } 166 | 167 | const img = await canvas.loadImage(imageUrl); 168 | const imgContext = imageCanvas.getContext("2d"); 169 | if (img.height > img.width) { 170 | const imgHeightMultiplier = imageCanvas.width / img.height; 171 | imgContext.drawImage( 172 | img, 173 | (imageCanvas.width - img.width * imgHeightMultiplier) / 2, 174 | 0, 175 | img.width * imgHeightMultiplier, 176 | imageCanvas.height 177 | ); 178 | } else { 179 | const imgWidthMultiplier = imageCanvas.height / img.width; 180 | imgContext.drawImage( 181 | img, 182 | 0, 183 | (imageCanvas.height - img.height * imgWidthMultiplier) / 2, 184 | imageCanvas.width, 185 | img.height * imgWidthMultiplier 186 | ); 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /img-generator/generator.ts: -------------------------------------------------------------------------------- 1 | import type * as anchor from "@project-serum/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import * as canvas from "canvas"; 4 | 5 | import { secondaryConnectionFor } from "../common/connection"; 6 | import type { TokenData } from "../common/tokenData"; 7 | import { getTokenData } from "../common/tokenData"; 8 | import { getTicketImage } from "./event-ticket-image"; 9 | import { 10 | drawBackgroundImage, 11 | drawDefaultBackground, 12 | drawLogo, 13 | drawShadow, 14 | drawText, 15 | } from "./img-utils"; 16 | import { getJamboImage } from "./jambo-image"; 17 | import { getNamespaceImage } from "./namespace-image"; 18 | import { tryDrawStakedOverlay } from "./staked-token"; 19 | 20 | export async function getImage( 21 | mintIdParam: string, 22 | nameParam: string, 23 | imgUriParam: string, 24 | textParam: string, 25 | cluster: string | null 26 | ): Promise { 27 | console.log( 28 | `Handling img generatation for mintIdParam (${mintIdParam}) imgUriParam (${imgUriParam}) text (${textParam}) and cluster (${ 29 | cluster ? cluster : "" 30 | })` 31 | ); 32 | 33 | const connection = secondaryConnectionFor(cluster); 34 | let tokenData: TokenData = {}; 35 | if (mintIdParam) { 36 | try { 37 | tokenData = await getTokenData(connection, new PublicKey(mintIdParam)); 38 | } catch (e) { 39 | console.log(e); 40 | } 41 | } 42 | if ( 43 | !tokenData.metaplexData && 44 | !tokenData.certificateData && 45 | !tokenData.tokenManagerData && 46 | !tokenData.timeInvalidatorData && 47 | !tokenData.useInvalidatorData && 48 | cluster !== "devnet" 49 | ) { 50 | console.log("Falling back to devnet image"); 51 | return getImage(mintIdParam, nameParam, imgUriParam, textParam, "devnet"); 52 | } 53 | 54 | if ( 55 | (tokenData?.metaplexData?.parsed.data.symbol === "NAME" || 56 | tokenData?.metaplexData?.parsed.data.symbol === "TICKET" || 57 | tokenData?.metaplexData?.parsed.data.symbol === "TIX") && 58 | tokenData?.metaplexData?.parsed.data.name.startsWith("crd-") 59 | ) { 60 | return getTicketImage(tokenData); 61 | } 62 | 63 | if ( 64 | tokenData?.metaplexData?.parsed.data.symbol === "NAME" || 65 | (textParam && textParam.includes("@")) 66 | ) { 67 | return getNamespaceImage(tokenData, nameParam, textParam); 68 | } 69 | 70 | if (tokenData?.metaplexData?.parsed.data.symbol === "$JAMB") { 71 | return getJamboImage(tokenData, connection, textParam, imgUriParam); 72 | } 73 | 74 | if (tokenData?.metaplexData?.parsed.data.symbol.startsWith("POOl")) { 75 | const img = await tryDrawStakedOverlay(tokenData, imgUriParam); 76 | if (img) { 77 | return img; 78 | } 79 | } 80 | 81 | // setup 82 | const WIDTH = 250; 83 | const HEIGHT = 250; 84 | const imageCanvas = canvas.createCanvas(WIDTH, HEIGHT); 85 | 86 | // draw base image 87 | if (imgUriParam) { 88 | await drawBackgroundImage(imageCanvas, imgUriParam); 89 | if (textParam) { 90 | drawText(imageCanvas, textParam, { 91 | defaultStyle: "overlay", 92 | }); 93 | } 94 | } else { 95 | drawDefaultBackground(imageCanvas); 96 | drawText( 97 | imageCanvas, 98 | textParam || tokenData?.metaplexData?.parsed?.data?.name || "", 99 | { defaultStyle: "none" } 100 | ); 101 | } 102 | 103 | if (tokenData.tokenManagerData) { 104 | drawShadow(imageCanvas, tokenData.tokenManagerData.parsed.state); 105 | } 106 | 107 | await drawLogo(imageCanvas); 108 | 109 | // draw badges 110 | const PADDING = 0.05 * WIDTH; 111 | const bottomLeftCtx = imageCanvas.getContext("2d"); 112 | bottomLeftCtx.textAlign = "left"; 113 | let bottomLeft = PADDING * 1.5; 114 | 115 | canvas.registerFont(__dirname.concat("/fonts/SF-Pro.ttf"), { 116 | family: "SFPro", 117 | }); 118 | const expiration = 119 | tokenData.timeInvalidatorData?.parsed?.expiration || 120 | (tokenData?.certificateData?.parsed?.expiration as anchor.BN | null); 121 | if (expiration) { 122 | if (expiration.toNumber() <= Math.floor(Date.now() / 1000)) { 123 | const dateTime = new Date(expiration.toNumber() * 1000); 124 | bottomLeftCtx.font = `${0.055 * WIDTH}px SFPro`; 125 | bottomLeftCtx.fillStyle = "rgba(255,0,0,1)"; 126 | bottomLeftCtx.fillText( 127 | `INVALID ${dateTime.toLocaleDateString(["en-US"], { 128 | month: "2-digit", 129 | day: "2-digit", 130 | year: "2-digit", 131 | })} ${dateTime.toLocaleTimeString(["en-US"], { 132 | hour: "2-digit", 133 | minute: "2-digit", 134 | timeZone: "UTC", 135 | timeZoneName: "short", 136 | })}`, 137 | PADDING, 138 | HEIGHT - bottomLeft 139 | ); 140 | } else { 141 | const dateTime = new Date(expiration.toNumber() * 1000); 142 | bottomLeftCtx.font = `${0.055 * WIDTH}px SFPro`; 143 | bottomLeftCtx.fillStyle = "white"; 144 | bottomLeftCtx.fillText( 145 | `${dateTime.toLocaleDateString(["en-US"], { 146 | month: "2-digit", 147 | day: "2-digit", 148 | year: "2-digit", 149 | })} ${dateTime.toLocaleTimeString(["en-US"], { 150 | hour: "2-digit", 151 | minute: "2-digit", 152 | timeZone: "UTC", 153 | timeZoneName: "short", 154 | })}`, 155 | PADDING, 156 | HEIGHT - bottomLeft 157 | ); 158 | } 159 | bottomLeft += 0.075 * WIDTH; 160 | } 161 | 162 | const durationSeconds = 163 | tokenData.timeInvalidatorData?.parsed?.durationSeconds; 164 | if (durationSeconds && durationSeconds.toNumber()) { 165 | const dateTime = new Date( 166 | (Date.now() / 1000 + durationSeconds.toNumber()) * 1000 167 | ); 168 | bottomLeftCtx.font = `${0.055 * WIDTH}px SFPro`; 169 | bottomLeftCtx.fillStyle = "white"; 170 | bottomLeftCtx.fillText( 171 | `${dateTime.toLocaleDateString(["en-US"], { 172 | month: "2-digit", 173 | day: "2-digit", 174 | year: "2-digit", 175 | })} ${dateTime.toLocaleTimeString(["en-US"], { 176 | hour: "2-digit", 177 | minute: "2-digit", 178 | timeZone: "UTC", 179 | timeZoneName: "short", 180 | })}`, 181 | PADDING, 182 | HEIGHT - bottomLeft 183 | ); 184 | bottomLeft += 0.075 * WIDTH; 185 | } 186 | 187 | const usages = 188 | tokenData.useInvalidatorData?.parsed?.usages || 189 | tokenData?.certificateData?.parsed?.usages; 190 | const maxUsages = 191 | tokenData.useInvalidatorData?.parsed?.maxUsages || 192 | (tokenData?.certificateData?.parsed?.maxUsages as anchor.BN | null); 193 | if (usages) { 194 | if (maxUsages && usages >= maxUsages) { 195 | bottomLeftCtx.font = `${0.055 * WIDTH}px SFPro`; 196 | bottomLeftCtx.fillStyle = "rgba(255,0,0,1)"; 197 | bottomLeftCtx.fillText( 198 | `INVALID (${usages.toNumber()}/${maxUsages.toString()})`, 199 | PADDING, 200 | HEIGHT - bottomLeft 201 | ); 202 | bottomLeft += 0.075 * WIDTH; 203 | } else { 204 | bottomLeftCtx.font = `${0.055 * WIDTH}px SFPro`; 205 | bottomLeftCtx.fillStyle = "white"; 206 | bottomLeftCtx.fillText( 207 | `Used (${usages.toNumber()}${ 208 | maxUsages ? `/${maxUsages.toString()}` : "" 209 | })`, 210 | PADDING, 211 | HEIGHT - bottomLeft 212 | ); 213 | bottomLeft += 0.075 * WIDTH; 214 | } 215 | } 216 | 217 | return imageCanvas.toBuffer("image/png"); 218 | } 219 | -------------------------------------------------------------------------------- /metadata-generator/generator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-assignment*/ 5 | import * as namespaces from "@solana-nft-programs/namespaces"; 6 | import type { Idl } from "@project-serum/anchor"; 7 | import { BorshAccountsCoder } from "@project-serum/anchor"; 8 | import * as web3 from "@solana/web3.js"; 9 | import fetch from "node-fetch"; 10 | 11 | import { secondaryConnectionFor } from "../common/connection"; 12 | import { tryGetEvent, tryGetEventTicket } from "../common/firebase"; 13 | import type { TokenData } from "../common/tokenData"; 14 | import { getTokenData } from "../common/tokenData"; 15 | import { getOwner } from "../common/utils"; 16 | import { getTicketImageURL } from "../img-generator/event-ticket-image"; 17 | import { 18 | durationAttributes, 19 | expirationAttributes, 20 | stateAttributes, 21 | typeAttributes, 22 | usageAttributes, 23 | } from "./attributes"; 24 | import { getDefaultMetadata } from "./default"; 25 | import { getTicketMetadataLink } from "./event-ticket-metadata"; 26 | import { getExpiredMetadata } from "./expired"; 27 | import { getDefaultTicketMetadata } from "./ticket"; 28 | import { getTwitterMetadata } from "./twitter"; 29 | 30 | export type NFTMetadata = { 31 | name?: string; 32 | symbol?: string; 33 | description?: string; 34 | seller_fee_basis_points?: number; 35 | external_url?: string; 36 | attributes?: any[]; 37 | collection?: any; 38 | properties?: any; 39 | image?: string; 40 | }; 41 | 42 | export async function getMetadata( 43 | mintId: string, 44 | nameParam: string, 45 | uriParam: string, 46 | textParam: string, 47 | imgParam: string, 48 | eventParam: string, 49 | attrs: string, 50 | cluster: string 51 | ): Promise { 52 | console.log( 53 | `Getting metadata for mintId (${mintId}) uri (${uriParam}) textParam (${textParam}) imgParam (${imgParam}) eventParam (${eventParam}) attrsParam (${attrs}) cluster (${cluster})` 54 | ); 55 | const connection = secondaryConnectionFor(cluster); 56 | let tokenData: TokenData = {}; 57 | try { 58 | tokenData = await getTokenData( 59 | connection, 60 | new web3.PublicKey(mintId), 61 | true 62 | ); 63 | } catch (e) { 64 | console.log(e); 65 | } 66 | if ( 67 | !tokenData.metaplexData && 68 | !tokenData.certificateData && 69 | !tokenData.tokenManagerData && 70 | !tokenData.timeInvalidatorData && 71 | !tokenData.useInvalidatorData 72 | ) { 73 | if (cluster !== "devnet") { 74 | console.log("Falling back to devnet metadata"); 75 | return getMetadata( 76 | mintId, 77 | nameParam, 78 | uriParam, 79 | textParam, 80 | imgParam, 81 | eventParam, 82 | attrs, 83 | "devnet" 84 | ); 85 | } 86 | } 87 | 88 | if ( 89 | !tokenData.tokenManagerData && 90 | tokenData.metaplexData?.parsed.data.symbol === "RCP" 91 | ) { 92 | return getExpiredMetadata(cluster); 93 | } 94 | 95 | const dynamicAttributes: { 96 | display_type: string; 97 | value: string; 98 | trait_type: string; 99 | }[] = []; 100 | if (attrs) { 101 | try { 102 | const attributeGroups = attrs.split(";"); 103 | for (let i = 0; i < attributeGroups.length; i++) { 104 | const attributeGroup = attributeGroups[i]; 105 | try { 106 | const scopes = attributeGroup.split("."); 107 | if (scopes.length > 1) { 108 | // scoped fields 109 | const [address, accountType, fieldGroupParam] = scopes; 110 | const accountInfo = await connection.getAccountInfo( 111 | new web3.PublicKey(address) 112 | ); 113 | const programId = accountInfo?.owner!; 114 | const IDL = (await import( 115 | `../idls/${programId.toString()}.json` 116 | )) as Idl; 117 | const coder = new BorshAccountsCoder(IDL); 118 | const scopeData = coder.decode(accountType, accountInfo?.data!); 119 | const fieldGroupStrings = 120 | fieldGroupParam === "*" 121 | ? Object.keys(scopeData) 122 | : fieldGroupParam.split(","); 123 | fieldGroupStrings.forEach((fieldGroupString) => { 124 | const fieldGroup = fieldGroupString.split(":"); 125 | if (fieldGroup[1] in scopeData || fieldGroup[0] in scopeData) { 126 | dynamicAttributes.push({ 127 | display_type: fieldGroup[0], 128 | value: scopeData[fieldGroup[1] ?? fieldGroup[0]].toString(), 129 | trait_type: fieldGroup[2] ?? fieldGroup[0], 130 | }); 131 | } 132 | }); 133 | } else { 134 | // inline fields 135 | const fieldGroup = scopes[0].split(":"); 136 | dynamicAttributes.push({ 137 | display_type: fieldGroup[0], 138 | value: fieldGroup[1] ?? fieldGroup[0], 139 | trait_type: fieldGroup[2] ?? fieldGroup[0], 140 | }); 141 | } 142 | } catch (e) { 143 | console.log("Failed to parse attribute: ", attributeGroup, e); 144 | } 145 | } 146 | } catch (e) { 147 | console.log("Failed to parse attributes", e); 148 | } 149 | } 150 | 151 | // ovverride uri with originalMint uri if present 152 | const originalMint = tokenData?.certificateData?.parsed 153 | .originalMint as web3.PublicKey; 154 | let originalTokenData: TokenData | null = null; 155 | if (originalMint) { 156 | try { 157 | originalTokenData = await getTokenData(connection, originalMint, true); 158 | } catch (e) { 159 | console.log( 160 | `Error fetching metaplex metadata for original mint (${originalMint.toString()})`, 161 | e 162 | ); 163 | } 164 | } 165 | 166 | if ( 167 | (tokenData?.metaplexData?.parsed.data.symbol === "NAME" || 168 | tokenData?.metaplexData?.parsed.data.symbol === "TICKET" || 169 | tokenData?.metaplexData?.parsed.data.symbol === "TIX") && 170 | (tokenData?.metaplexData?.parsed.data.name.startsWith("crd-") || 171 | nameParam.startsWith("crd-")) 172 | ) { 173 | const metadataUri = getTicketMetadataLink( 174 | tokenData?.metaplexData?.parsed.data.name 175 | ); 176 | const imageUri = getTicketImageURL( 177 | tokenData?.metaplexData?.parsed.data.name 178 | ); 179 | try { 180 | const metadataResponse = await fetch(metadataUri, {}); 181 | if (metadataResponse.status !== 200) { 182 | throw new Error("Metadata not found"); 183 | } 184 | const metadata = (await metadataResponse.json()) as NFTMetadata; 185 | 186 | const ticketid = tokenData?.metaplexData?.parsed.data.name; 187 | const ticketData = await tryGetEventTicket(ticketid); 188 | if (ticketData) { 189 | const eventId = ticketData.eventId; 190 | const eventData = await tryGetEvent(eventId); 191 | metadata.collection = { 192 | name: eventData?.name, 193 | family: eventData?.name, 194 | }; 195 | 196 | metadata.attributes = [ 197 | ...(metadata.attributes || []), 198 | ...typeAttributes(tokenData), 199 | ...usageAttributes(tokenData), 200 | ...expirationAttributes(tokenData), 201 | { 202 | trait_type: "verified", 203 | value: 204 | tokenData?.metaplexData.parsed.data.creators?.find( 205 | (c) => c.verified 206 | )?.address === ticketData.ticketSignerAddress 207 | ? "True" 208 | : "False", 209 | display_type: "Verified", 210 | }, 211 | ]; 212 | 213 | if (eventData) { 214 | const verifyUrl = `https://events.host.so/default/${eventData?.shortLink}/verify`; 215 | metadata.external_url = `https://phantom.app/ul/browse/${encodeURIComponent( 216 | verifyUrl 217 | )}`; 218 | } 219 | } 220 | return { ...metadata, image: imageUri }; 221 | } catch (e) { 222 | return getDefaultTicketMetadata(mintId, cluster, [ 223 | ...typeAttributes(tokenData), 224 | ...usageAttributes(tokenData), 225 | ...expirationAttributes(tokenData), 226 | ]); 227 | } 228 | } 229 | 230 | if (tokenData?.metaplexData?.parsed.data.symbol === "NAME") { 231 | const mintName = 232 | originalTokenData?.metaplexData?.parsed.data.name || 233 | tokenData?.metaplexData?.parsed.data.name; 234 | const namespace = nameParam 235 | ? tokenData?.metaplexData?.parsed.data.name 236 | : namespaces.breakName(mintName || textParam || "")[0]; 237 | 238 | if (namespace === "twitter") { 239 | const owner = await getOwner(secondaryConnectionFor(cluster), mintId); 240 | return getTwitterMetadata( 241 | nameParam ? `${nameParam}.${namespace}` : mintName, 242 | mintId, 243 | owner.toString(), 244 | nameParam, 245 | cluster 246 | ); 247 | } else { 248 | const metadataUri = `https://events.host.so/events/${namespace}/event.json`; 249 | try { 250 | const metadataResponse = await fetch(metadataUri, {}); 251 | if (metadataResponse.status !== 200) { 252 | throw new Error("Metadata not found"); 253 | } 254 | const metadata = await metadataResponse.json(); 255 | return metadata as NFTMetadata; 256 | } catch (e) { 257 | return getDefaultMetadata( 258 | namespace, 259 | mintName, 260 | mintId, 261 | nameParam, 262 | cluster 263 | ); 264 | } 265 | } 266 | } 267 | 268 | let response: NFTMetadata = { 269 | attributes: [], 270 | }; 271 | const metadataUri = eventParam 272 | ? `https://events.host.so/events/${eventParam}/event.json` 273 | : uriParam; 274 | if (originalTokenData?.metadata || metadataUri || tokenData.metadata) { 275 | let metadata = 276 | originalTokenData?.metadata?.data || tokenData.metadata?.data; 277 | if (!metadata) { 278 | try { 279 | metadata = await fetch(metadataUri, {}).then((r: Response) => r.json()); 280 | } catch (e) { 281 | console.log("Failed to get metadata URI"); 282 | } 283 | } 284 | 285 | if (metadata) { 286 | response = { 287 | ...response, 288 | ...metadata, 289 | }; 290 | } 291 | 292 | if ( 293 | (metadata && 294 | (tokenData?.certificateData || tokenData.tokenManagerData)) || 295 | imgParam || 296 | textParam 297 | ) { 298 | response = { 299 | ...response, 300 | ...metadata, 301 | image: `https://nft.host.so/img/${mintId}?uri=${ 302 | metadata?.image || "" 303 | }${textParam ? `&text=${textParam}` : ""}${ 304 | nameParam ? `&name=${encodeURIComponent(nameParam)}` : "" 305 | }${cluster ? `&cluster=${cluster}` : ""}`, 306 | }; 307 | } 308 | } 309 | 310 | response = { 311 | ...response, 312 | attributes: [ 313 | ...(response.attributes || []), 314 | ...stateAttributes(tokenData), 315 | ...typeAttributes(tokenData), 316 | ...usageAttributes(tokenData), 317 | ...expirationAttributes(tokenData), 318 | ...durationAttributes(tokenData), 319 | ], 320 | }; 321 | 322 | // collection 323 | if (tokenData?.metaplexData?.parsed.data.symbol === "$JAMB") { 324 | response = { 325 | ...response, 326 | collection: { 327 | name: "Jambomambo", 328 | family: "Jambomambo", 329 | }, 330 | description: 331 | textParam === "TRAINING" 332 | ? "This Origin Jambo is out training in Jambo Camp!" 333 | : "This Origin Jambo is out hunting for loot around Jambo Camp!", 334 | }; 335 | } 336 | 337 | // collection 338 | if (tokenData?.metaplexData?.parsed.data.symbol === "RCP") { 339 | response = { 340 | ...response, 341 | collection: { 342 | name: "Receipts", 343 | family: "Receipts", 344 | }, 345 | }; 346 | } 347 | 348 | if (dynamicAttributes) { 349 | response = { 350 | ...response, 351 | attributes: [...(response.attributes || []), ...dynamicAttributes], 352 | }; 353 | } 354 | 355 | if (imgParam) { 356 | response = { 357 | ...response, 358 | image: imgParam, 359 | }; 360 | } 361 | 362 | if (nameParam) { 363 | response = { 364 | ...response, 365 | name: nameParam, 366 | }; 367 | } 368 | 369 | return response; 370 | } 371 | -------------------------------------------------------------------------------- /common/tokenData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty */ 2 | import type { CertificateData } from "@solana-nft-programs/certificates"; 3 | import { 4 | CERTIFICATE_IDL, 5 | CERTIFICATE_PROGRAM_ID, 6 | certificateIdForMint, 7 | } from "@solana-nft-programs/certificates"; 8 | import { getBatchedMultipleAccounts } from "@solana-nft-programs/common"; 9 | import type { AccountData } from "@solana-nft-programs/token-manager"; 10 | import { 11 | timeInvalidator, 12 | useInvalidator, 13 | } from "@solana-nft-programs/token-manager/dist/cjs/programs"; 14 | import type { PaidClaimApproverData } from "@solana-nft-programs/token-manager/dist/cjs/programs/claimApprover"; 15 | import { 16 | CLAIM_APPROVER_ADDRESS, 17 | CLAIM_APPROVER_IDL, 18 | } from "@solana-nft-programs/token-manager/dist/cjs/programs/claimApprover"; 19 | import type { TimeInvalidatorData } from "@solana-nft-programs/token-manager/dist/cjs/programs/timeInvalidator"; 20 | import { 21 | TIME_INVALIDATOR_ADDRESS, 22 | TIME_INVALIDATOR_IDL, 23 | } from "@solana-nft-programs/token-manager/dist/cjs/programs/timeInvalidator"; 24 | import type { TokenManagerData } from "@solana-nft-programs/token-manager/dist/cjs/programs/tokenManager"; 25 | import { 26 | TOKEN_MANAGER_ADDRESS, 27 | TOKEN_MANAGER_IDL, 28 | } from "@solana-nft-programs/token-manager/dist/cjs/programs/tokenManager"; 29 | import { findTokenManagerAddress } from "@solana-nft-programs/token-manager/dist/cjs/programs/tokenManager/pda"; 30 | import type { UseInvalidatorData } from "@solana-nft-programs/token-manager/dist/cjs/programs/useInvalidator"; 31 | import { 32 | USE_INVALIDATOR_ADDRESS, 33 | USE_INVALIDATOR_IDL, 34 | } from "@solana-nft-programs/token-manager/dist/cjs/programs/useInvalidator"; 35 | import * as metaplex from "@metaplex-foundation/mpl-token-metadata"; 36 | import { 37 | MasterEditionV1Data, 38 | MasterEditionV2Data, 39 | MetadataKey, 40 | } from "@metaplex-foundation/mpl-token-metadata"; 41 | import { BorshAccountsCoder } from "@project-serum/anchor"; 42 | import * as spl from "@solana/spl-token"; 43 | import type { 44 | AccountInfo, 45 | Connection, 46 | ParsedAccountData, 47 | PublicKey, 48 | } from "@solana/web3.js"; 49 | import fetch from "node-fetch"; 50 | 51 | import type { NFTMetadata } from "../metadata-generator/generator"; 52 | 53 | export type TokenData = { 54 | tokenManagerData?: AccountData | null; 55 | metaplexData?: AccountData | null; 56 | useInvalidatorData?: AccountData | null; 57 | timeInvalidatorData?: AccountData | null; 58 | certificateData?: AccountData | null; 59 | metadata?: { pubkey: PublicKey; data: NFTMetadata } | null; 60 | }; 61 | 62 | export type AccountType = 63 | | "metaplexMetadata" 64 | | "editionData" 65 | | "tokenManager" 66 | | "tokenAccount" 67 | | "timeInvalidator" 68 | | "paidClaimApprover" 69 | | "useInvalidator" 70 | | "certificate"; 71 | 72 | export type AccountTypeData = { 73 | type: AccountType; 74 | displayName?: string; 75 | }; 76 | 77 | export type AccountDataById = { 78 | [accountId: string]: 79 | | (AccountData & AccountInfo & AccountTypeData) 80 | | (AccountData & AccountInfo & AccountTypeData) 81 | | (AccountData & 82 | AccountInfo & 83 | AccountTypeData) 84 | | (AccountData & AccountInfo & AccountTypeData) 85 | | (AccountData & AccountInfo & AccountTypeData) 86 | | (spl.AccountInfo & AccountTypeData) 87 | | (AccountData & 88 | AccountInfo & 89 | AccountTypeData) 90 | | (AccountData & 91 | AccountInfo & 92 | AccountTypeData) 93 | | (AccountData & 94 | AccountInfo & 95 | AccountTypeData) 96 | | (AccountData & AccountInfo & AccountTypeData); 97 | }; 98 | 99 | export const deserializeAccountInfos = ( 100 | accountIds: (PublicKey | null)[], 101 | accountInfos: (AccountInfo | null)[] 102 | ): AccountDataById => { 103 | return accountInfos.reduce((acc, accountInfo, i) => { 104 | const ownerString = accountInfo?.owner.toString(); 105 | switch (ownerString) { 106 | case CERTIFICATE_PROGRAM_ID.toString(): 107 | try { 108 | const type = "certificate"; 109 | const coder = new BorshAccountsCoder(CERTIFICATE_IDL); 110 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 111 | const parsed = coder.decode( 112 | type, 113 | accountInfo?.data as Buffer 114 | ) as CertificateData; 115 | acc[accountIds[i]!.toString()] = { 116 | type, 117 | pubkey: accountIds[i]!, 118 | ...(accountInfo as AccountInfo), 119 | parsed, 120 | }; 121 | } catch (e) {} 122 | return acc; 123 | case TOKEN_MANAGER_ADDRESS.toString(): 124 | try { 125 | const type = "tokenManager"; 126 | const coder = new BorshAccountsCoder(TOKEN_MANAGER_IDL); 127 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 128 | const parsed = coder.decode( 129 | type, 130 | accountInfo?.data as Buffer 131 | ) as TokenManagerData; 132 | acc[accountIds[i]!.toString()] = { 133 | type, 134 | pubkey: accountIds[i]!, 135 | ...(accountInfo as AccountInfo), 136 | parsed, 137 | }; 138 | } catch (e) {} 139 | return acc; 140 | case TIME_INVALIDATOR_ADDRESS.toString(): 141 | try { 142 | const type = "timeInvalidator"; 143 | const coder = new BorshAccountsCoder(TIME_INVALIDATOR_IDL); 144 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 145 | const parsed = coder.decode( 146 | type, 147 | accountInfo?.data as Buffer 148 | ) as TimeInvalidatorData; 149 | acc[accountIds[i]!.toString()] = { 150 | type, 151 | pubkey: accountIds[i]!, 152 | ...(accountInfo as AccountInfo), 153 | parsed, 154 | }; 155 | } catch (e) {} 156 | return acc; 157 | case USE_INVALIDATOR_ADDRESS.toString(): 158 | try { 159 | const type = "useInvalidator"; 160 | const coder = new BorshAccountsCoder(USE_INVALIDATOR_IDL); 161 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 162 | const parsed = coder.decode( 163 | type, 164 | accountInfo?.data as Buffer 165 | ) as UseInvalidatorData; 166 | acc[accountIds[i]!.toString()] = { 167 | type, 168 | pubkey: accountIds[i]!, 169 | ...(accountInfo as AccountInfo), 170 | parsed, 171 | }; 172 | } catch (e) {} 173 | return acc; 174 | case CLAIM_APPROVER_ADDRESS.toString(): 175 | try { 176 | const type = "paidClaimApprover"; 177 | const coder = new BorshAccountsCoder(CLAIM_APPROVER_IDL); 178 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 179 | const parsed = coder.decode( 180 | type, 181 | accountInfo?.data as Buffer 182 | ) as PaidClaimApproverData; 183 | acc[accountIds[i]!.toString()] = { 184 | type, 185 | pubkey: accountIds[i]!, 186 | ...(accountInfo as AccountInfo), 187 | parsed, 188 | }; 189 | } catch (e) {} 190 | return acc; 191 | case spl.TOKEN_PROGRAM_ID.toString(): 192 | try { 193 | acc[accountIds[i]!.toString()] = { 194 | type: "tokenAccount", 195 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 196 | ...((accountInfo?.data as ParsedAccountData).parsed 197 | ?.info as spl.AccountInfo), 198 | }; 199 | } catch (e) {} 200 | return acc; 201 | case metaplex.MetadataProgram.PUBKEY.toString(): 202 | try { 203 | acc[accountIds[i]!.toString()] = { 204 | type: "metaplexMetadata", 205 | pubkey: accountIds[i]!, 206 | parsed: metaplex.MetadataData.deserialize( 207 | accountInfo?.data as Buffer 208 | ) as metaplex.MetadataData, 209 | ...(accountInfo as AccountInfo), 210 | }; 211 | } catch (e) {} 212 | try { 213 | const key = 214 | accountInfo === null || accountInfo === void 0 215 | ? void 0 216 | : (accountInfo.data as Buffer)[0]; 217 | let parsed; 218 | if (key === MetadataKey.EditionV1) { 219 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 220 | parsed = MasterEditionV1Data.deserialize( 221 | accountInfo?.data as Buffer 222 | ); 223 | } else if ( 224 | key === MetadataKey.MasterEditionV1 || 225 | key === MetadataKey.MasterEditionV2 226 | ) { 227 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 228 | parsed = MasterEditionV2Data.deserialize( 229 | accountInfo?.data as Buffer 230 | ); 231 | } 232 | if (parsed) { 233 | acc[accountIds[i]!.toString()] = { 234 | type: "editionData", 235 | pubkey: accountIds[i]!, 236 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 237 | parsed, 238 | ...(accountInfo as AccountInfo), 239 | }; 240 | } 241 | } catch (e) {} 242 | return acc; 243 | default: 244 | return acc; 245 | } 246 | }, {} as AccountDataById); 247 | }; 248 | 249 | export const accountDataById = async ( 250 | connection: Connection, 251 | ids: (PublicKey | null)[] 252 | ): Promise => { 253 | const filteredIds = ids.filter((id): id is PublicKey => id !== null); 254 | const accountInfos = await getBatchedMultipleAccounts( 255 | connection, 256 | filteredIds, 257 | { encoding: "jsonParsed" } 258 | ); 259 | return deserializeAccountInfos(filteredIds, accountInfos); 260 | }; 261 | 262 | export async function getTokenData( 263 | connection: Connection, 264 | mintId: PublicKey, 265 | getMetadata = false 266 | ): Promise { 267 | const [[metaplexId], [tokenManagerId], [certificateId]] = await Promise.all([ 268 | metaplex.MetadataProgram.find_metadata_account(mintId), 269 | findTokenManagerAddress(mintId), 270 | certificateIdForMint(mintId), 271 | ]); 272 | 273 | const [[timeInvalidatorId], [useInvalidatorId]] = await Promise.all([ 274 | timeInvalidator.pda.findTimeInvalidatorAddress(tokenManagerId), 275 | useInvalidator.pda.findUseInvalidatorAddress(tokenManagerId), 276 | ]); 277 | 278 | const accountsById = await accountDataById(connection, [ 279 | metaplexId, 280 | tokenManagerId, 281 | timeInvalidatorId, 282 | useInvalidatorId, 283 | certificateId, 284 | ]); 285 | 286 | let metadata: { pubkey: PublicKey; data: NFTMetadata } | null = null; 287 | const metaplexData = accountsById[ 288 | metaplexId.toString() 289 | ] as AccountData; 290 | if ( 291 | metaplexData && 292 | getMetadata && 293 | !metaplexData.parsed.data.uri.includes("host") 294 | ) { 295 | try { 296 | const uri = metaplexData.parsed.data.uri; 297 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 298 | const json = (await fetch(uri).then((r: Response) => 299 | r.json() 300 | )) as NFTMetadata; 301 | metadata = { 302 | pubkey: metaplexData.pubkey, 303 | data: json, 304 | }; 305 | } catch (e) { 306 | console.log("Failed to get metadata data", e); 307 | } 308 | } 309 | 310 | return { 311 | metaplexData, 312 | useInvalidatorData: useInvalidatorId 313 | ? (accountsById[ 314 | useInvalidatorId.toString() 315 | ] as AccountData) 316 | : undefined, 317 | timeInvalidatorData: timeInvalidatorId 318 | ? (accountsById[ 319 | timeInvalidatorId.toString() 320 | ] as AccountData) 321 | : undefined, 322 | tokenManagerData: tokenManagerId 323 | ? (accountsById[ 324 | tokenManagerId.toString() 325 | ] as AccountData) 326 | : undefined, 327 | certificateData: certificateId 328 | ? (accountsById[certificateId.toString()] as AccountData) 329 | : undefined, 330 | metadata, 331 | }; 332 | } 333 | -------------------------------------------------------------------------------- /idls/LVLYTWmTaRCV5JcZ5HQkU1bhEjx34xqGiT3eWU6SuX9.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "stake_pool", 4 | "instructions": [ 5 | { 6 | "name": "initEntry", 7 | "accounts": [ 8 | { 9 | "name": "stakeEntry", 10 | "isMut": true, 11 | "isSigner": false 12 | }, 13 | { 14 | "name": "originalMint", 15 | "isMut": false, 16 | "isSigner": false 17 | }, 18 | { 19 | "name": "payer", 20 | "isMut": false, 21 | "isSigner": true 22 | }, 23 | { 24 | "name": "certificateMint", 25 | "isMut": true, 26 | "isSigner": false 27 | }, 28 | { 29 | "name": "certificateMintTokenAccount", 30 | "isMut": true, 31 | "isSigner": false 32 | }, 33 | { 34 | "name": "certifiacteMintMetadata", 35 | "isMut": true, 36 | "isSigner": false 37 | }, 38 | { 39 | "name": "mintManager", 40 | "isMut": true, 41 | "isSigner": false 42 | }, 43 | { 44 | "name": "certificateProgram", 45 | "isMut": false, 46 | "isSigner": false 47 | }, 48 | { 49 | "name": "tokenMetadataProgram", 50 | "isMut": false, 51 | "isSigner": false 52 | }, 53 | { 54 | "name": "tokenProgram", 55 | "isMut": false, 56 | "isSigner": false 57 | }, 58 | { 59 | "name": "associatedToken", 60 | "isMut": false, 61 | "isSigner": false 62 | }, 63 | { 64 | "name": "rent", 65 | "isMut": false, 66 | "isSigner": false 67 | }, 68 | { 69 | "name": "systemProgram", 70 | "isMut": false, 71 | "isSigner": false 72 | } 73 | ], 74 | "args": [ 75 | { 76 | "name": "ix", 77 | "type": { 78 | "defined": "InitializeEntryIx" 79 | } 80 | } 81 | ] 82 | }, 83 | { 84 | "name": "stake", 85 | "accounts": [ 86 | { 87 | "name": "stakeEntry", 88 | "isMut": true, 89 | "isSigner": false 90 | }, 91 | { 92 | "name": "originalMint", 93 | "isMut": false, 94 | "isSigner": false 95 | }, 96 | { 97 | "name": "certificateMint", 98 | "isMut": true, 99 | "isSigner": false 100 | }, 101 | { 102 | "name": "stakeEntryOriginalMintTokenAccount", 103 | "isMut": true, 104 | "isSigner": false 105 | }, 106 | { 107 | "name": "stakeEntryCertificateMintTokenAccount", 108 | "isMut": true, 109 | "isSigner": false 110 | }, 111 | { 112 | "name": "user", 113 | "isMut": true, 114 | "isSigner": true 115 | }, 116 | { 117 | "name": "userOriginalMintTokenAccount", 118 | "isMut": true, 119 | "isSigner": false 120 | }, 121 | { 122 | "name": "userCertificateMintTokenAccount", 123 | "isMut": true, 124 | "isSigner": false 125 | }, 126 | { 127 | "name": "mintManager", 128 | "isMut": false, 129 | "isSigner": false 130 | }, 131 | { 132 | "name": "certificate", 133 | "isMut": true, 134 | "isSigner": false 135 | }, 136 | { 137 | "name": "certificateTokenAccount", 138 | "isMut": true, 139 | "isSigner": false 140 | }, 141 | { 142 | "name": "tokenProgram", 143 | "isMut": false, 144 | "isSigner": false 145 | }, 146 | { 147 | "name": "certificateProgram", 148 | "isMut": false, 149 | "isSigner": false 150 | }, 151 | { 152 | "name": "associatedToken", 153 | "isMut": false, 154 | "isSigner": false 155 | }, 156 | { 157 | "name": "rent", 158 | "isMut": false, 159 | "isSigner": false 160 | }, 161 | { 162 | "name": "systemProgram", 163 | "isMut": false, 164 | "isSigner": false 165 | } 166 | ], 167 | "args": [ 168 | { 169 | "name": "ix", 170 | "type": { 171 | "defined": "StakeIx" 172 | } 173 | } 174 | ] 175 | }, 176 | { 177 | "name": "unstake", 178 | "accounts": [ 179 | { 180 | "name": "stakeEntry", 181 | "isMut": true, 182 | "isSigner": false 183 | }, 184 | { 185 | "name": "originalMint", 186 | "isMut": false, 187 | "isSigner": false 188 | }, 189 | { 190 | "name": "certificateMint", 191 | "isMut": false, 192 | "isSigner": false 193 | }, 194 | { 195 | "name": "stakeEntryOriginalMintTokenAccount", 196 | "isMut": true, 197 | "isSigner": false 198 | }, 199 | { 200 | "name": "stakeEntryCertificateMintTokenAccount", 201 | "isMut": true, 202 | "isSigner": false 203 | }, 204 | { 205 | "name": "user", 206 | "isMut": true, 207 | "isSigner": true 208 | }, 209 | { 210 | "name": "userOriginalMintTokenAccount", 211 | "isMut": true, 212 | "isSigner": false 213 | }, 214 | { 215 | "name": "userCertificateMintTokenAccount", 216 | "isMut": true, 217 | "isSigner": false 218 | }, 219 | { 220 | "name": "tokenProgram", 221 | "isMut": false, 222 | "isSigner": false 223 | } 224 | ], 225 | "args": [] 226 | }, 227 | { 228 | "name": "group", 229 | "accounts": [ 230 | { 231 | "name": "groupEntry", 232 | "isMut": true, 233 | "isSigner": false 234 | }, 235 | { 236 | "name": "stakeEntryOne", 237 | "isMut": true, 238 | "isSigner": false 239 | }, 240 | { 241 | "name": "stakeEntryTwo", 242 | "isMut": true, 243 | "isSigner": false 244 | }, 245 | { 246 | "name": "stakeEntryThree", 247 | "isMut": true, 248 | "isSigner": false 249 | }, 250 | { 251 | "name": "stakeEntryFour", 252 | "isMut": true, 253 | "isSigner": false 254 | }, 255 | { 256 | "name": "staker", 257 | "isMut": false, 258 | "isSigner": true 259 | }, 260 | { 261 | "name": "payer", 262 | "isMut": true, 263 | "isSigner": true 264 | }, 265 | { 266 | "name": "systemProgram", 267 | "isMut": false, 268 | "isSigner": false 269 | } 270 | ], 271 | "args": [ 272 | { 273 | "name": "ix", 274 | "type": { 275 | "defined": "GroupStakeIx" 276 | } 277 | } 278 | ] 279 | }, 280 | { 281 | "name": "ungroup", 282 | "accounts": [ 283 | { 284 | "name": "groupEntry", 285 | "isMut": true, 286 | "isSigner": false 287 | }, 288 | { 289 | "name": "stakeEntryOne", 290 | "isMut": true, 291 | "isSigner": false 292 | }, 293 | { 294 | "name": "stakeEntryTwo", 295 | "isMut": true, 296 | "isSigner": false 297 | }, 298 | { 299 | "name": "stakeEntryThree", 300 | "isMut": true, 301 | "isSigner": false 302 | }, 303 | { 304 | "name": "stakeEntryFour", 305 | "isMut": true, 306 | "isSigner": false 307 | }, 308 | { 309 | "name": "authority", 310 | "isMut": true, 311 | "isSigner": true 312 | } 313 | ], 314 | "args": [] 315 | } 316 | ], 317 | "accounts": [ 318 | { 319 | "name": "StakeEntry", 320 | "type": { 321 | "kind": "struct", 322 | "fields": [ 323 | { 324 | "name": "bump", 325 | "type": "u8" 326 | }, 327 | { 328 | "name": "originalMint", 329 | "type": "publicKey" 330 | }, 331 | { 332 | "name": "certificateMint", 333 | "type": "publicKey" 334 | }, 335 | { 336 | "name": "totalStakeSeconds", 337 | "type": "i64" 338 | }, 339 | { 340 | "name": "lastStakedAt", 341 | "type": "i64" 342 | }, 343 | { 344 | "name": "stakeBoost", 345 | "type": "u64" 346 | }, 347 | { 348 | "name": "lastStaker", 349 | "type": "publicKey" 350 | }, 351 | { 352 | "name": "tribe", 353 | "type": "string" 354 | }, 355 | { 356 | "name": "hungry", 357 | "type": "bool" 358 | }, 359 | { 360 | "name": "stakeGroup", 361 | "type": { 362 | "option": "publicKey" 363 | } 364 | } 365 | ] 366 | } 367 | }, 368 | { 369 | "name": "GroupStakeEntry", 370 | "type": { 371 | "kind": "struct", 372 | "fields": [ 373 | { 374 | "name": "bump", 375 | "type": "u8" 376 | }, 377 | { 378 | "name": "stakeEntryOne", 379 | "type": "publicKey" 380 | }, 381 | { 382 | "name": "stakeEntryTwo", 383 | "type": "publicKey" 384 | }, 385 | { 386 | "name": "stakeEntryThree", 387 | "type": "publicKey" 388 | }, 389 | { 390 | "name": "stakeEntryFour", 391 | "type": "publicKey" 392 | }, 393 | { 394 | "name": "staker", 395 | "type": "publicKey" 396 | } 397 | ] 398 | } 399 | } 400 | ], 401 | "types": [ 402 | { 403 | "name": "GroupStakeIx", 404 | "type": { 405 | "kind": "struct", 406 | "fields": [ 407 | { 408 | "name": "bump", 409 | "type": "u8" 410 | } 411 | ] 412 | } 413 | }, 414 | { 415 | "name": "InitializeEntryIx", 416 | "type": { 417 | "kind": "struct", 418 | "fields": [ 419 | { 420 | "name": "bump", 421 | "type": "u8" 422 | }, 423 | { 424 | "name": "mintManagerBump", 425 | "type": "u8" 426 | }, 427 | { 428 | "name": "name", 429 | "type": "string" 430 | }, 431 | { 432 | "name": "symbol", 433 | "type": "string" 434 | }, 435 | { 436 | "name": "tribe", 437 | "type": "string" 438 | }, 439 | { 440 | "name": "hungry", 441 | "type": "bool" 442 | } 443 | ] 444 | } 445 | }, 446 | { 447 | "name": "StakeIx", 448 | "type": { 449 | "kind": "struct", 450 | "fields": [ 451 | { 452 | "name": "certificateBump", 453 | "type": "u8" 454 | } 455 | ] 456 | } 457 | } 458 | ], 459 | "errors": [ 460 | { 461 | "code": 6000, 462 | "name": "InvalidOriginalMint", 463 | "msg": "Original mint is invalid" 464 | }, 465 | { 466 | "code": 6001, 467 | "name": "InvalidCertificateMint", 468 | "msg": "Certificate mint is invalid" 469 | }, 470 | { 471 | "code": 6002, 472 | "name": "InvalidUserTokenAccountOwner", 473 | "msg": "User must own token account" 474 | }, 475 | { 476 | "code": 6003, 477 | "name": "InvalidUserOriginalMintTokenAccount", 478 | "msg": "Invalid user original mint token account" 479 | }, 480 | { 481 | "code": 6004, 482 | "name": "InvalidUserCertificateMintTokenAccount", 483 | "msg": "Invalid user certificate mint account" 484 | }, 485 | { 486 | "code": 6005, 487 | "name": "InvalidStakeEntryOriginalMintTokenAccount", 488 | "msg": "Invalid stake entry original mint token account" 489 | }, 490 | { 491 | "code": 6006, 492 | "name": "InvalidStakeEntryCertificateMintTokenAccount", 493 | "msg": "Invalid stake entry certificate mint token account" 494 | }, 495 | { 496 | "code": 6007, 497 | "name": "InvalidUnstakeUser", 498 | "msg": "Invalid unstake user only last staker can unstake" 499 | }, 500 | { 501 | "code": 6008, 502 | "name": "CannotGroupStake", 503 | "msg": "Group stake requires four currently staked tokens of different tribes" 504 | }, 505 | { 506 | "code": 6009, 507 | "name": "GroupUnstakeEntryDoesntMatch", 508 | "msg": "Stake entry doesn't belong to stake group" 509 | }, 510 | { 511 | "code": 6010, 512 | "name": "StakeEntryAlreadyGrouped", 513 | "msg": "Stake entry is already grouped" 514 | }, 515 | { 516 | "code": 6011, 517 | "name": "StakeEntryIsPartOfStakeGroup", 518 | "msg": "Cannot unstake because stake entry is part of a stake group" 519 | }, 520 | { 521 | "code": 6012, 522 | "name": "InvalidAuthority", 523 | "msg": "Invalid stake pool authority" 524 | } 525 | ] 526 | } -------------------------------------------------------------------------------- /idls/LVLYTWmTaRCV5JcZ5HQkU1bhEjx34xqGiT3eWU6SuX9.ts: -------------------------------------------------------------------------------- 1 | export type StakePool = { 2 | version: "0.0.0"; 3 | name: "stake_pool"; 4 | instructions: [ 5 | { 6 | name: "initEntry"; 7 | accounts: [ 8 | { 9 | name: "stakeEntry"; 10 | isMut: true; 11 | isSigner: false; 12 | }, 13 | { 14 | name: "originalMint"; 15 | isMut: false; 16 | isSigner: false; 17 | }, 18 | { 19 | name: "payer"; 20 | isMut: true; 21 | isSigner: true; 22 | }, 23 | { 24 | name: "certificateMint"; 25 | isMut: true; 26 | isSigner: false; 27 | }, 28 | { 29 | name: "certificateMintTokenAccount"; 30 | isMut: true; 31 | isSigner: false; 32 | }, 33 | { 34 | name: "certifiacteMintMetadata"; 35 | isMut: true; 36 | isSigner: false; 37 | }, 38 | { 39 | name: "mintManager"; 40 | isMut: true; 41 | isSigner: false; 42 | }, 43 | { 44 | name: "certificateProgram"; 45 | isMut: false; 46 | isSigner: false; 47 | }, 48 | { 49 | name: "tokenMetadataProgram"; 50 | isMut: false; 51 | isSigner: false; 52 | }, 53 | { 54 | name: "tokenProgram"; 55 | isMut: false; 56 | isSigner: false; 57 | }, 58 | { 59 | name: "associatedToken"; 60 | isMut: false; 61 | isSigner: false; 62 | }, 63 | { 64 | name: "rent"; 65 | isMut: false; 66 | isSigner: false; 67 | }, 68 | { 69 | name: "systemProgram"; 70 | isMut: false; 71 | isSigner: false; 72 | } 73 | ]; 74 | args: [ 75 | { 76 | name: "ix"; 77 | type: { 78 | defined: "InitializeEntryIx"; 79 | }; 80 | } 81 | ]; 82 | }, 83 | { 84 | name: "stake"; 85 | accounts: [ 86 | { 87 | name: "stakeEntry"; 88 | isMut: true; 89 | isSigner: false; 90 | }, 91 | { 92 | name: "originalMint"; 93 | isMut: false; 94 | isSigner: false; 95 | }, 96 | { 97 | name: "certificateMint"; 98 | isMut: true; 99 | isSigner: false; 100 | }, 101 | { 102 | name: "stakeEntryOriginalMintTokenAccount"; 103 | isMut: true; 104 | isSigner: false; 105 | }, 106 | { 107 | name: "stakeEntryCertificateMintTokenAccount"; 108 | isMut: true; 109 | isSigner: false; 110 | }, 111 | { 112 | name: "user"; 113 | isMut: true; 114 | isSigner: true; 115 | }, 116 | { 117 | name: "userOriginalMintTokenAccount"; 118 | isMut: true; 119 | isSigner: false; 120 | }, 121 | { 122 | name: "userCertificateMintTokenAccount"; 123 | isMut: true; 124 | isSigner: false; 125 | }, 126 | { 127 | name: "mintManager"; 128 | isMut: false; 129 | isSigner: false; 130 | }, 131 | { 132 | name: "certificate"; 133 | isMut: true; 134 | isSigner: false; 135 | }, 136 | { 137 | name: "certificateTokenAccount"; 138 | isMut: true; 139 | isSigner: false; 140 | }, 141 | { 142 | name: "tokenProgram"; 143 | isMut: false; 144 | isSigner: false; 145 | }, 146 | { 147 | name: "certificateProgram"; 148 | isMut: false; 149 | isSigner: false; 150 | }, 151 | { 152 | name: "associatedToken"; 153 | isMut: false; 154 | isSigner: false; 155 | }, 156 | { 157 | name: "rent"; 158 | isMut: false; 159 | isSigner: false; 160 | }, 161 | { 162 | name: "systemProgram"; 163 | isMut: false; 164 | isSigner: false; 165 | } 166 | ]; 167 | args: [ 168 | { 169 | name: "ix"; 170 | type: { 171 | defined: "StakeIx"; 172 | }; 173 | } 174 | ]; 175 | }, 176 | { 177 | name: "unstake"; 178 | accounts: [ 179 | { 180 | name: "stakeEntry"; 181 | isMut: true; 182 | isSigner: false; 183 | }, 184 | { 185 | name: "originalMint"; 186 | isMut: false; 187 | isSigner: false; 188 | }, 189 | { 190 | name: "certificateMint"; 191 | isMut: false; 192 | isSigner: false; 193 | }, 194 | { 195 | name: "stakeEntryOriginalMintTokenAccount"; 196 | isMut: true; 197 | isSigner: false; 198 | }, 199 | { 200 | name: "stakeEntryCertificateMintTokenAccount"; 201 | isMut: true; 202 | isSigner: false; 203 | }, 204 | { 205 | name: "user"; 206 | isMut: true; 207 | isSigner: true; 208 | }, 209 | { 210 | name: "userOriginalMintTokenAccount"; 211 | isMut: true; 212 | isSigner: false; 213 | }, 214 | { 215 | name: "userCertificateMintTokenAccount"; 216 | isMut: true; 217 | isSigner: false; 218 | }, 219 | { 220 | name: "tokenProgram"; 221 | isMut: false; 222 | isSigner: false; 223 | } 224 | ]; 225 | args: []; 226 | }, 227 | { 228 | name: "group"; 229 | accounts: [ 230 | { 231 | name: "groupEntry"; 232 | isMut: true; 233 | isSigner: false; 234 | }, 235 | { 236 | name: "stakeEntryOne"; 237 | isMut: true; 238 | isSigner: false; 239 | }, 240 | { 241 | name: "stakeEntryTwo"; 242 | isMut: true; 243 | isSigner: false; 244 | }, 245 | { 246 | name: "stakeEntryThree"; 247 | isMut: true; 248 | isSigner: false; 249 | }, 250 | { 251 | name: "stakeEntryFour"; 252 | isMut: true; 253 | isSigner: false; 254 | }, 255 | { 256 | name: "staker"; 257 | isMut: false; 258 | isSigner: true; 259 | }, 260 | { 261 | name: "payer"; 262 | isMut: true; 263 | isSigner: true; 264 | }, 265 | { 266 | name: "systemProgram"; 267 | isMut: false; 268 | isSigner: false; 269 | } 270 | ]; 271 | args: [ 272 | { 273 | name: "ix"; 274 | type: { 275 | defined: "GroupStakeIx"; 276 | }; 277 | } 278 | ]; 279 | }, 280 | { 281 | name: "ungroup"; 282 | accounts: [ 283 | { 284 | name: "groupEntry"; 285 | isMut: true; 286 | isSigner: false; 287 | }, 288 | { 289 | name: "stakeEntryOne"; 290 | isMut: true; 291 | isSigner: false; 292 | }, 293 | { 294 | name: "stakeEntryTwo"; 295 | isMut: true; 296 | isSigner: false; 297 | }, 298 | { 299 | name: "stakeEntryThree"; 300 | isMut: true; 301 | isSigner: false; 302 | }, 303 | { 304 | name: "stakeEntryFour"; 305 | isMut: true; 306 | isSigner: false; 307 | }, 308 | { 309 | name: "authority"; 310 | isMut: true; 311 | isSigner: true; 312 | } 313 | ]; 314 | args: []; 315 | } 316 | ]; 317 | accounts: [ 318 | { 319 | name: "stakeEntry"; 320 | type: { 321 | kind: "struct"; 322 | fields: [ 323 | { 324 | name: "bump"; 325 | type: "u8"; 326 | }, 327 | { 328 | name: "originalMint"; 329 | type: "publicKey"; 330 | }, 331 | { 332 | name: "certificateMint"; 333 | type: "publicKey"; 334 | }, 335 | { 336 | name: "totalStakeSeconds"; 337 | type: "i64"; 338 | }, 339 | { 340 | name: "lastStakedAt"; 341 | type: "i64"; 342 | }, 343 | { 344 | name: "stakeBoost"; 345 | type: "u64"; 346 | }, 347 | { 348 | name: "lastStaker"; 349 | type: "publicKey"; 350 | }, 351 | { 352 | name: "tribe"; 353 | type: "string"; 354 | }, 355 | { 356 | name: "hungry"; 357 | type: "bool"; 358 | }, 359 | { 360 | name: "stakeGroup"; 361 | type: { 362 | option: "publicKey"; 363 | }; 364 | } 365 | ]; 366 | }; 367 | }, 368 | { 369 | name: "groupStakeEntry"; 370 | type: { 371 | kind: "struct"; 372 | fields: [ 373 | { 374 | name: "bump"; 375 | type: "u8"; 376 | }, 377 | { 378 | name: "stakeEntryOne"; 379 | type: "publicKey"; 380 | }, 381 | { 382 | name: "stakeEntryTwo"; 383 | type: "publicKey"; 384 | }, 385 | { 386 | name: "stakeEntryThree"; 387 | type: "publicKey"; 388 | }, 389 | { 390 | name: "stakeEntryFour"; 391 | type: "publicKey"; 392 | }, 393 | { 394 | name: "staker"; 395 | type: "publicKey"; 396 | } 397 | ]; 398 | }; 399 | } 400 | ]; 401 | types: [ 402 | { 403 | name: "GroupStakeIx"; 404 | type: { 405 | kind: "struct"; 406 | fields: [ 407 | { 408 | name: "bump"; 409 | type: "u8"; 410 | } 411 | ]; 412 | }; 413 | }, 414 | { 415 | name: "InitializeEntryIx"; 416 | type: { 417 | kind: "struct"; 418 | fields: [ 419 | { 420 | name: "bump"; 421 | type: "u8"; 422 | }, 423 | { 424 | name: "mintManagerBump"; 425 | type: "u8"; 426 | }, 427 | { 428 | name: "name"; 429 | type: "string"; 430 | }, 431 | { 432 | name: "symbol"; 433 | type: "string"; 434 | }, 435 | { 436 | name: "tribe"; 437 | type: "string"; 438 | }, 439 | { 440 | name: "hungry"; 441 | type: "bool"; 442 | } 443 | ]; 444 | }; 445 | }, 446 | { 447 | name: "StakeIx"; 448 | type: { 449 | kind: "struct"; 450 | fields: [ 451 | { 452 | name: "certificateBump"; 453 | type: "u8"; 454 | } 455 | ]; 456 | }; 457 | } 458 | ]; 459 | errors: [ 460 | { 461 | code: 300; 462 | name: "InvalidOriginalMint"; 463 | msg: "Original mint is invalid"; 464 | }, 465 | { 466 | code: 301; 467 | name: "InvalidCertificateMint"; 468 | msg: "Certificate mint is invalid"; 469 | }, 470 | { 471 | code: 302; 472 | name: "InvalidUserTokenAccountOwner"; 473 | msg: "User must own token account"; 474 | }, 475 | { 476 | code: 303; 477 | name: "InvalidUserOriginalMintTokenAccount"; 478 | msg: "Invalid user original mint token account"; 479 | }, 480 | { 481 | code: 304; 482 | name: "InvalidUserCertificateMintTokenAccount"; 483 | msg: "Invalid user certificate mint account"; 484 | }, 485 | { 486 | code: 305; 487 | name: "InvalidStakeEntryOriginalMintTokenAccount"; 488 | msg: "Invalid stake entry original mint token account"; 489 | }, 490 | { 491 | code: 306; 492 | name: "InvalidStakeEntryCertificateMintTokenAccount"; 493 | msg: "Invalid stake entry certificate mint token account"; 494 | }, 495 | { 496 | code: 307; 497 | name: "InvalidUnstakeUser"; 498 | msg: "Invalid unstake user only last staker can unstake"; 499 | }, 500 | { 501 | code: 308; 502 | name: "CannotGroupStake"; 503 | msg: "Group stake requires four currently staked tokens of different tribes"; 504 | }, 505 | { 506 | code: 309; 507 | name: "GroupUnstakeEntryDoesntMatch"; 508 | msg: "Stake entry doesn't belong to stake group"; 509 | }, 510 | { 511 | code: 310; 512 | name: "StakeEntryAlreadyGrouped"; 513 | msg: "Stake entry is already grouped"; 514 | }, 515 | { 516 | code: 311; 517 | name: "StakeEntryIsPartOfStakeGroup"; 518 | msg: "Cannot unstake because stake entry is part of a stake group"; 519 | } 520 | ]; 521 | }; 522 | 523 | export const IDL: StakePool = { 524 | version: "0.0.0", 525 | name: "stake_pool", 526 | instructions: [ 527 | { 528 | name: "initEntry", 529 | accounts: [ 530 | { 531 | name: "stakeEntry", 532 | isMut: true, 533 | isSigner: false, 534 | }, 535 | { 536 | name: "originalMint", 537 | isMut: false, 538 | isSigner: false, 539 | }, 540 | { 541 | name: "payer", 542 | isMut: true, 543 | isSigner: true, 544 | }, 545 | { 546 | name: "certificateMint", 547 | isMut: true, 548 | isSigner: false, 549 | }, 550 | { 551 | name: "certificateMintTokenAccount", 552 | isMut: true, 553 | isSigner: false, 554 | }, 555 | { 556 | name: "certifiacteMintMetadata", 557 | isMut: true, 558 | isSigner: false, 559 | }, 560 | { 561 | name: "mintManager", 562 | isMut: true, 563 | isSigner: false, 564 | }, 565 | { 566 | name: "certificateProgram", 567 | isMut: false, 568 | isSigner: false, 569 | }, 570 | { 571 | name: "tokenMetadataProgram", 572 | isMut: false, 573 | isSigner: false, 574 | }, 575 | { 576 | name: "tokenProgram", 577 | isMut: false, 578 | isSigner: false, 579 | }, 580 | { 581 | name: "associatedToken", 582 | isMut: false, 583 | isSigner: false, 584 | }, 585 | { 586 | name: "rent", 587 | isMut: false, 588 | isSigner: false, 589 | }, 590 | { 591 | name: "systemProgram", 592 | isMut: false, 593 | isSigner: false, 594 | }, 595 | ], 596 | args: [ 597 | { 598 | name: "ix", 599 | type: { 600 | defined: "InitializeEntryIx", 601 | }, 602 | }, 603 | ], 604 | }, 605 | { 606 | name: "stake", 607 | accounts: [ 608 | { 609 | name: "stakeEntry", 610 | isMut: true, 611 | isSigner: false, 612 | }, 613 | { 614 | name: "originalMint", 615 | isMut: false, 616 | isSigner: false, 617 | }, 618 | { 619 | name: "certificateMint", 620 | isMut: true, 621 | isSigner: false, 622 | }, 623 | { 624 | name: "stakeEntryOriginalMintTokenAccount", 625 | isMut: true, 626 | isSigner: false, 627 | }, 628 | { 629 | name: "stakeEntryCertificateMintTokenAccount", 630 | isMut: true, 631 | isSigner: false, 632 | }, 633 | { 634 | name: "user", 635 | isMut: true, 636 | isSigner: true, 637 | }, 638 | { 639 | name: "userOriginalMintTokenAccount", 640 | isMut: true, 641 | isSigner: false, 642 | }, 643 | { 644 | name: "userCertificateMintTokenAccount", 645 | isMut: true, 646 | isSigner: false, 647 | }, 648 | { 649 | name: "mintManager", 650 | isMut: false, 651 | isSigner: false, 652 | }, 653 | { 654 | name: "certificate", 655 | isMut: true, 656 | isSigner: false, 657 | }, 658 | { 659 | name: "certificateTokenAccount", 660 | isMut: true, 661 | isSigner: false, 662 | }, 663 | { 664 | name: "tokenProgram", 665 | isMut: false, 666 | isSigner: false, 667 | }, 668 | { 669 | name: "certificateProgram", 670 | isMut: false, 671 | isSigner: false, 672 | }, 673 | { 674 | name: "associatedToken", 675 | isMut: false, 676 | isSigner: false, 677 | }, 678 | { 679 | name: "rent", 680 | isMut: false, 681 | isSigner: false, 682 | }, 683 | { 684 | name: "systemProgram", 685 | isMut: false, 686 | isSigner: false, 687 | }, 688 | ], 689 | args: [ 690 | { 691 | name: "ix", 692 | type: { 693 | defined: "StakeIx", 694 | }, 695 | }, 696 | ], 697 | }, 698 | { 699 | name: "unstake", 700 | accounts: [ 701 | { 702 | name: "stakeEntry", 703 | isMut: true, 704 | isSigner: false, 705 | }, 706 | { 707 | name: "originalMint", 708 | isMut: false, 709 | isSigner: false, 710 | }, 711 | { 712 | name: "certificateMint", 713 | isMut: false, 714 | isSigner: false, 715 | }, 716 | { 717 | name: "stakeEntryOriginalMintTokenAccount", 718 | isMut: true, 719 | isSigner: false, 720 | }, 721 | { 722 | name: "stakeEntryCertificateMintTokenAccount", 723 | isMut: true, 724 | isSigner: false, 725 | }, 726 | { 727 | name: "user", 728 | isMut: true, 729 | isSigner: true, 730 | }, 731 | { 732 | name: "userOriginalMintTokenAccount", 733 | isMut: true, 734 | isSigner: false, 735 | }, 736 | { 737 | name: "userCertificateMintTokenAccount", 738 | isMut: true, 739 | isSigner: false, 740 | }, 741 | { 742 | name: "tokenProgram", 743 | isMut: false, 744 | isSigner: false, 745 | }, 746 | ], 747 | args: [], 748 | }, 749 | { 750 | name: "group", 751 | accounts: [ 752 | { 753 | name: "groupEntry", 754 | isMut: true, 755 | isSigner: false, 756 | }, 757 | { 758 | name: "stakeEntryOne", 759 | isMut: true, 760 | isSigner: false, 761 | }, 762 | { 763 | name: "stakeEntryTwo", 764 | isMut: true, 765 | isSigner: false, 766 | }, 767 | { 768 | name: "stakeEntryThree", 769 | isMut: true, 770 | isSigner: false, 771 | }, 772 | { 773 | name: "stakeEntryFour", 774 | isMut: true, 775 | isSigner: false, 776 | }, 777 | { 778 | name: "staker", 779 | isMut: false, 780 | isSigner: true, 781 | }, 782 | { 783 | name: "payer", 784 | isMut: true, 785 | isSigner: true, 786 | }, 787 | { 788 | name: "systemProgram", 789 | isMut: false, 790 | isSigner: false, 791 | }, 792 | ], 793 | args: [ 794 | { 795 | name: "ix", 796 | type: { 797 | defined: "GroupStakeIx", 798 | }, 799 | }, 800 | ], 801 | }, 802 | { 803 | name: "ungroup", 804 | accounts: [ 805 | { 806 | name: "groupEntry", 807 | isMut: true, 808 | isSigner: false, 809 | }, 810 | { 811 | name: "stakeEntryOne", 812 | isMut: true, 813 | isSigner: false, 814 | }, 815 | { 816 | name: "stakeEntryTwo", 817 | isMut: true, 818 | isSigner: false, 819 | }, 820 | { 821 | name: "stakeEntryThree", 822 | isMut: true, 823 | isSigner: false, 824 | }, 825 | { 826 | name: "stakeEntryFour", 827 | isMut: true, 828 | isSigner: false, 829 | }, 830 | { 831 | name: "authority", 832 | isMut: true, 833 | isSigner: true, 834 | }, 835 | ], 836 | args: [], 837 | }, 838 | ], 839 | accounts: [ 840 | { 841 | name: "stakeEntry", 842 | type: { 843 | kind: "struct", 844 | fields: [ 845 | { 846 | name: "bump", 847 | type: "u8", 848 | }, 849 | { 850 | name: "originalMint", 851 | type: "publicKey", 852 | }, 853 | { 854 | name: "certificateMint", 855 | type: "publicKey", 856 | }, 857 | { 858 | name: "totalStakeSeconds", 859 | type: "i64", 860 | }, 861 | { 862 | name: "lastStakedAt", 863 | type: "i64", 864 | }, 865 | { 866 | name: "stakeBoost", 867 | type: "u64", 868 | }, 869 | { 870 | name: "lastStaker", 871 | type: "publicKey", 872 | }, 873 | { 874 | name: "tribe", 875 | type: "string", 876 | }, 877 | { 878 | name: "hungry", 879 | type: "bool", 880 | }, 881 | { 882 | name: "stakeGroup", 883 | type: { 884 | option: "publicKey", 885 | }, 886 | }, 887 | ], 888 | }, 889 | }, 890 | { 891 | name: "groupStakeEntry", 892 | type: { 893 | kind: "struct", 894 | fields: [ 895 | { 896 | name: "bump", 897 | type: "u8", 898 | }, 899 | { 900 | name: "stakeEntryOne", 901 | type: "publicKey", 902 | }, 903 | { 904 | name: "stakeEntryTwo", 905 | type: "publicKey", 906 | }, 907 | { 908 | name: "stakeEntryThree", 909 | type: "publicKey", 910 | }, 911 | { 912 | name: "stakeEntryFour", 913 | type: "publicKey", 914 | }, 915 | { 916 | name: "staker", 917 | type: "publicKey", 918 | }, 919 | ], 920 | }, 921 | }, 922 | ], 923 | types: [ 924 | { 925 | name: "GroupStakeIx", 926 | type: { 927 | kind: "struct", 928 | fields: [ 929 | { 930 | name: "bump", 931 | type: "u8", 932 | }, 933 | ], 934 | }, 935 | }, 936 | { 937 | name: "InitializeEntryIx", 938 | type: { 939 | kind: "struct", 940 | fields: [ 941 | { 942 | name: "bump", 943 | type: "u8", 944 | }, 945 | { 946 | name: "mintManagerBump", 947 | type: "u8", 948 | }, 949 | { 950 | name: "name", 951 | type: "string", 952 | }, 953 | { 954 | name: "symbol", 955 | type: "string", 956 | }, 957 | { 958 | name: "tribe", 959 | type: "string", 960 | }, 961 | { 962 | name: "hungry", 963 | type: "bool", 964 | }, 965 | ], 966 | }, 967 | }, 968 | { 969 | name: "StakeIx", 970 | type: { 971 | kind: "struct", 972 | fields: [ 973 | { 974 | name: "certificateBump", 975 | type: "u8", 976 | }, 977 | ], 978 | }, 979 | }, 980 | ], 981 | errors: [ 982 | { 983 | code: 300, 984 | name: "InvalidOriginalMint", 985 | msg: "Original mint is invalid", 986 | }, 987 | { 988 | code: 301, 989 | name: "InvalidCertificateMint", 990 | msg: "Certificate mint is invalid", 991 | }, 992 | { 993 | code: 302, 994 | name: "InvalidUserTokenAccountOwner", 995 | msg: "User must own token account", 996 | }, 997 | { 998 | code: 303, 999 | name: "InvalidUserOriginalMintTokenAccount", 1000 | msg: "Invalid user original mint token account", 1001 | }, 1002 | { 1003 | code: 304, 1004 | name: "InvalidUserCertificateMintTokenAccount", 1005 | msg: "Invalid user certificate mint account", 1006 | }, 1007 | { 1008 | code: 305, 1009 | name: "InvalidStakeEntryOriginalMintTokenAccount", 1010 | msg: "Invalid stake entry original mint token account", 1011 | }, 1012 | { 1013 | code: 306, 1014 | name: "InvalidStakeEntryCertificateMintTokenAccount", 1015 | msg: "Invalid stake entry certificate mint token account", 1016 | }, 1017 | { 1018 | code: 307, 1019 | name: "InvalidUnstakeUser", 1020 | msg: "Invalid unstake user only last staker can unstake", 1021 | }, 1022 | { 1023 | code: 308, 1024 | name: "CannotGroupStake", 1025 | msg: "Group stake requires four currently staked tokens of different tribes", 1026 | }, 1027 | { 1028 | code: 309, 1029 | name: "GroupUnstakeEntryDoesntMatch", 1030 | msg: "Stake entry doesn't belong to stake group", 1031 | }, 1032 | { 1033 | code: 310, 1034 | name: "StakeEntryAlreadyGrouped", 1035 | msg: "Stake entry is already grouped", 1036 | }, 1037 | { 1038 | code: 311, 1039 | name: "StakeEntryIsPartOfStakeGroup", 1040 | msg: "Cannot unstake because stake entry is part of a stake group", 1041 | }, 1042 | ], 1043 | }; 1044 | --------------------------------------------------------------------------------