Skip to content

Instantly share code, notes, and snippets.

@myyellowshoe
Last active August 28, 2023 20:28
Show Gist options
  • Save myyellowshoe/1a17d8e23c1a9e8282a418f26a5f65de to your computer and use it in GitHub Desktop.
Save myyellowshoe/1a17d8e23c1a9e8282a418f26a5f65de to your computer and use it in GitHub Desktop.
Solana NFT Image Transform

NFT Image Transfomer

I wanted a way to resize/cache nft images for speedy loading nfts on the front end so I build this. Mimics the functionality of cloudinary/ImageKit, but uses shadow drive to store the images. This is based on firebase functions, but could be adapted to any serverless platform or nodejs app. Also this is for the solana ecosystem but could be used for any blockchain given you have a way to get the image.

Current this only supports resizing, but could be expanded to support other transformations.

Setup

  1. Get a shadow drive account. https://www.shadow.cloud

  2. Install these pacakages: npm install jimp @shadow-drive/sdk @metaplex-foundation/js

  1. Optional but highly recommended: Signup with helius to get a free solana rpc endpoint. https://helius.network

  2. Add local environment variables for your shadow drive key and helius rpc key.

  • HELIUS_RPC_KEY
  • SHADOW_KEY

How it works

  • Hit the url with the mint address and transformations
  • The function checks shadowdrive to see if the image exists with the transformations applied
  • If the image exists, it returns the image from shadowdrive
  • If the image does not exist, it gets the image with metaplex, applies the transformations, saves it to shadowdrive, and returns the image

Structure & Usage https://whateverneatosite.com/transform/sol/{MINT_ADDRESS}/w-{number}_h-{number}?api-key={apikey}

Example Url: https://us-central1-coinboi-27940.cloudfunctions.net/transform/sol/8UsGRVd8miHUawQm9ECJSBeJ4t2q3UBsiJiF1c7p246Y/w-300_h-300?api-key=alloi-secret-club

In an image tag <img src="https://us-central1-coinboi-27940.cloudfunctions.net/transform/sol/8UsGRVd8miHUawQm9ECJSBeJ4t2q3UBsiJiF1c7p246Y/w-300_h-300?api-key=alloi-secret-club" />

import * as functions from "firebase-functions";
import { ShadowFile, ShdwDrive } from "@shadow-drive/sdk";
import { Wallet } from "@project-serum/anchor";
import { Metaplex } from "@metaplex-foundation/js";
import jimp from "jimp";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
const secretKey = Uint8Array.from(process.env.SHADOW_KEY as Iterable<number>);
const keypair = Keypair.fromSecretKey(secretKey);
const connection = new Connection(
`https://rpc.helius.xyz/?api-key=${process.env.HELIUS_RPC_KEY}`,
"confirmed"
);
const metaplex = Metaplex.make(connection);
const wallet = new Wallet(keypair);
type TTransform = {
match: string;
type: string;
value: string | number;
};
const supportedTrasnformations = [
{ match: "w", type: "width" },
{ match: "h", type: "height" },
];
export const transform = functions.https.onRequest(
async (request, response) => {
response.set("Access-Control-Allow-Origin", "*");
response.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.set("Access-Control-Allow-Headers", "Content-Type");
// Get the API key from headers or query parameters
const apiKey = request.headers["api-key"] || request.query["api-key"];
if (!apiKey) {
response.status(401).send("API key is required");
return;
}
// Validate the API key
const expectedApiKey = "alloi-secret-club";
if (apiKey !== expectedApiKey) {
response.status(403).send("Invalid API key");
return;
}
const drive = await new ShdwDrive(connection, wallet)
.init()
.catch((err) => {
console.log(err);
return err;
});
const accts = await drive.getStorageAccounts("v2");
const accountKey = "alloi";
let accountPublicKey;
// handle printing pubKey of first storage acct
const existingAccount = accts.find(
(r: any) => r.account.identifier === accountKey
);
if (!existingAccount) {
const account = await drive
.createStorageAccount(accountKey, "10MB", "v2")
.then((r: any) => {
console.log(r);
return r;
})
.catch((err: any) => {
console.log(err);
return err;
});
accountPublicKey = new PublicKey(account.shdw_bucket);
} else {
accountPublicKey = existingAccount.publicKey;
}
// Url alloi/transform/mintid/w-300_h-300
const parts = request.url.split("/");
const mintAddress = new PublicKey(parts[2]);
// Check shadowdrive
const fileName = `${parts[2]}__${parts[3]}`;
const imagePath = `https://shdw-drive.genesysgo.net/${accountPublicKey}/${fileName}`;
const existingImage = await jimp.read(imagePath).catch(() => {
return;
});
console.log(existingImage);
if (existingImage) {
response.set("Content-Type", existingImage.getMIME());
const buffer = await existingImage.getBufferAsync(jimp.MIME_PNG);
response.send(buffer);
return;
}
// Get transformations based on supported list
const transformations = parts[3]
.split("_")
.map((r) => {
const match = r.split("-")[0];
console.log(
"match",
match,
supportedTrasnformations.find((x) => x.match === match)
);
const transform = {
...supportedTrasnformations.find((x) => x.match === match),
value: r.split("-")[1],
} as TTransform;
console.log(transform);
if (!transform?.type) {
return null;
}
return transform;
})
.filter((r) => r !== null);
// Get mint address
// See if the path matches an exisiting image based on the transform props
// If so return cached image from shadow drive
// if not process image,
// save to shadowdrive
// return it
const nft = await metaplex.nfts().findByMint({ mintAddress });
if (!nft.json || !nft.json.image) {
console.error("Image not found");
response.json({ error: "Image not found" });
return;
}
const nftImage = nft.json.image;
// Read the image.
const image = await jimp.read(nftImage);
// Handle resize
const width = transformations.find((r) => r?.type === "width");
const height = transformations.find((r) => r?.type === "height");
if (width && height) {
await image.resize(Number(width.value), Number(height.value));
} else if (width) {
await image.resize(Number(width.value), jimp.AUTO);
} else if (height) {
await image.resize(jimp.AUTO, Number(height.value));
}
const buffer = await image.getBufferAsync(jimp.MIME_PNG);
const fileToUpload: ShadowFile = {
name: `${fileName}`,
file: buffer,
};
console.log("uploading file");
const uploadFile = await drive.uploadFile(accountPublicKey, fileToUpload);
console.log(uploadFile);
response.set("Content-Type", image.getMIME());
response.send(buffer);
return;
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment