├── .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 |
--------------------------------------------------------------------------------