Skip to content

Instantly share code, notes, and snippets.

@jongan69
Created January 20, 2025 20:52
Show Gist options
  • Save jongan69/78f734c793552a1a6bf0a77e22094e09 to your computer and use it in GitHub Desktop.
Save jongan69/78f734c793552a1a6bf0a77e22094e09 to your computer and use it in GitHub Desktop.
Using Metaplex UMI for token data, reference lockin repo tokenUtils to see original
import { AddressLookupTableAccount, Connection, PublicKey, PublicKeyInitData, TransactionInstruction } from "@solana/web3.js";
// import { Metaplex } from "@metaplex-foundation/js";
import Bottleneck from "bottleneck";
import {
fetchDigitalAssetWithAssociatedToken,
mplTokenMetadata,
findMetadataPda} from '@metaplex-foundation/mpl-token-metadata'
import { fromWeb3JsPublicKey, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { fetchIpfsMetadata } from "./fetchIpfsMetadata";
import { extractCidFromUrl } from "./extractCidFromUrl";
import { fetchJupiterSwap } from "./fetchJupiterSwap";
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { LOCKIN_MINT, TOKEN_PROGRAM_ID_ADDRESS } from "@utils/globals";
import { Instruction } from "@jup-ag/api";
import { fetchFloorPrice } from "./fetchFloorPrice";
import { NETWORK } from "@utils/endpoints";
import { createJupiterApiClient, QuoteGetRequest } from "@jup-ag/api";
import { getTokenInfo } from "./fetchDexTokenInfo";
const ENDPOINT = NETWORK;
// Add validation for the endpoint URL
if (!ENDPOINT || (!ENDPOINT.startsWith('http:') && !ENDPOINT.startsWith('https:'))) {
// console.log(`ENDPOINT: ${ENDPOINT}`);
throw new Error('Invalid RPC endpoint URL. Must start with http: or https:');
}
// console.log(`ENDPOINT: ${ENDPOINT}`);
const connection = new Connection(ENDPOINT);
// const metaplex = Metaplex.make(connection);
const DEFAULT_IMAGE_URL = process.env.UNKNOWN_IMAGE_URL || "https://s3.coinmarketcap.com/static-gravity/image/5cc0b99a8dd84fbfa4e150d84b5531f2.png";
// Modify the rate limiters at the top
const rpcLimiter = new Bottleneck({
maxConcurrent: 5, // Reduce concurrent requests
minTime: 200, // Increase delay between requests
reservoir: 30, // Initial tokens
reservoirRefreshAmount: 30, // Tokens to refresh
reservoirRefreshInterval: 1000, // Refresh every second
retryCount: 3, // Number of retries
retryDelay: 1000, // Delay between retries
});
export const apiLimiter = new Bottleneck({
maxConcurrent: 3, // Reduce concurrent requests
minTime: 333, // About 3 requests per second
reservoir: 20, // Initial tokens
reservoirRefreshAmount: 20, // Tokens to refresh
reservoirRefreshInterval: 1000, // Refresh every second
retryCount: 3,
retryDelay: 1000,
});
// Add retry wrapper function
const withRetry = async <T>(operation: () => Promise<T>, maxRetries: number = 3, delayMs: number = 1000): Promise<T | null> => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error: any) {
lastError = error;
if (error.toString().includes('rate limit') || error.toString().includes('429')) {
console.log(`Rate limit hit, attempt ${i + 1}/${maxRetries}, waiting ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs * (i + 1))); // Exponential backoff
continue;
}
console.warn("Error fetching data:", error);
}
}
console.warn("Last error:", lastError);
return null;
};
// Add these constants at the top
const MAX_TOKEN_FETCH_RETRIES = 2;
const TOKEN_FETCH_RETRY_DELAY = 1000;
// Add a retry wrapper function
const withTokenRetry = async <T>(
operation: () => Promise<T>,
tokenIdentifier: string
): Promise<T | null> => {
let attempts = 0;
while (attempts < MAX_TOKEN_FETCH_RETRIES) {
try {
return await operation();
} catch (error) {
attempts++;
console.error(`Attempt ${attempts} failed for token ${tokenIdentifier}:`, error);
if (attempts < MAX_TOKEN_FETCH_RETRIES) {
await new Promise(resolve => setTimeout(resolve, TOKEN_FETCH_RETRY_DELAY));
continue;
}
console.log(`Skipping token ${tokenIdentifier} after ${attempts} failed attempts`);
return null;
}
}
return null;
};
export type TokenData = {
decimals: number;
mintAddress: string;
tokenAddress: string;
name?: string;
amount: number;
symbol?: string;
logo?: string;
cid?: string | null;
usdValue: number;
collectionName?: string;
collectionLogo?: string;
isNft?: boolean;
swappable: boolean;
};
export async function fetchTokenMetadata(mintAddress: PublicKey, mint: string, amount: number) {
try {
// console.log("πŸ” Starting fetchTokenMetadata for mint:", mint);
const umi = createUmi(ENDPOINT).use(mplTokenMetadata());
// console.log("πŸ“₯ Fetching digital asset with associated token...");
const asset = await fetchDigitalAssetWithAssociatedToken(
umi,
fromWeb3JsPublicKey(mintAddress),
fromWeb3JsPublicKey(mintAddress)
);
if (!asset) {
// console.log("⚠️ Token account not found, returning default metadata");
return getDefaultTokenMetadata(mint, amount);
}
const cid = extractCidFromUrl(asset.metadata.uri);
const IPFSMetadata = await apiLimiter.schedule(() =>
fetchIpfsMetadata(cid as string)
);
console.log(asset.metadata.collection.__option)
// Extract metadata from the digital asset
let metadata = {
name: asset.metadata.name || mint,
symbol: asset.metadata.symbol || mint,
logo: IPFSMetadata.imageUrl ?? DEFAULT_IMAGE_URL,
cid: extractCidFromUrl(asset.metadata.uri),
collectionName: asset.metadata.name || mint,
collectionLogo: IPFSMetadata.imageUrl ?? DEFAULT_IMAGE_URL,
isNft: asset.metadata.collection.__option === "None" ? false : true
};
// Handle collection metadata
if (asset.metadata.collection && 'key' in asset.metadata.collection) {
try {
const collectionKey = fromWeb3JsPublicKey(new PublicKey(asset.metadata.collection.key as string));
const collectionMetadata = await fetchCollectionMetadata(new PublicKey(collectionKey.toString()));
metadata = {
...metadata,
collectionName: collectionMetadata?.name ?? metadata.name,
collectionLogo: collectionMetadata?.logo ?? metadata.logo,
isNft: true
};
} catch (collectionError) {
console.warn(`Failed to fetch collection metadata for token ${mint}:`, collectionError);
}
}
return metadata;
} catch (error) {
console.warn("❌ Error in fetchTokenMetadata:", error);
return getDefaultTokenMetadata(mint, amount);
}
}
async function fetchCollectionMetadata(collectionAddress: PublicKey) {
// console.log("πŸ” Starting fetchCollectionMetadata for:", collectionAddress.toString());
try {
const umi = createUmi(ENDPOINT).use(mplTokenMetadata());
// console.log("πŸ“₯ Fetching collection asset...");
const collectionAsset = await fetchDigitalAssetWithAssociatedToken(
umi,
fromWeb3JsPublicKey(collectionAddress),
fromWeb3JsPublicKey(collectionAddress)
);
const metadataPda = findMetadataPda(umi, { mint: fromWeb3JsPublicKey(collectionAddress) });
const metadataAccountInfo = await withRetry(() =>
rpcLimiter.schedule(() =>
connection.getAccountInfo(toWeb3JsPublicKey(metadataPda[0]))
)
);
if (!metadataAccountInfo) {
// console.log(`No metadata account found for collection: ${collectionAddress.toString()}`);
return getDefaultMetadata();
}
const cid = extractCidFromUrl(collectionAsset.metadata.uri);
if (cid) {
try {
const collectionMetadata = await apiLimiter.schedule(() =>
fetchIpfsMetadata(cid)
);
return {
name: collectionAsset.metadata.name || "Unknown Collection",
symbol: collectionAsset.metadata.symbol || "UNKNOWN",
logo: collectionMetadata.imageUrl ?? collectionAsset.metadata.uri ?? DEFAULT_IMAGE_URL,
cid: cid,
isNft: true
};
} catch (ipfsError) {
console.warn(`Failed to fetch IPFS metadata for collection ${collectionAddress.toString()}:`, ipfsError);
return {
name: collectionAsset.metadata.name || "Unknown Collection",
symbol: collectionAsset.metadata.symbol || "UNKNOWN",
logo: collectionAsset.metadata.uri ?? DEFAULT_IMAGE_URL,
cid: cid,
isNft: true
};
}
}
return {
name: collectionAsset.metadata.name || "Unknown Collection",
symbol: collectionAsset.metadata.symbol || "UNKNOWN",
logo: collectionAsset.metadata.uri ?? DEFAULT_IMAGE_URL,
cid: null,
isNft: true
};
} catch (error) {
console.error("❌ Error in fetchCollectionMetadata:", error);
return getDefaultMetadata();
}
}
// Add a helper function to return default metadata
function getDefaultMetadata() {
// console.log("Returning default metadata");
return {
name: "Unknown Collection",
symbol: "UNKNOWN",
logo: DEFAULT_IMAGE_URL,
cid: null,
isNft: true
};
}
// Helper function to get default token metadata
async function getDefaultTokenMetadata(mint: string, amount: number) {
// console.log("Returning default token metadata for:", mint);
const tokenInfo = await getTokenInfo(mint);
// console.log("DexScreener Token info:", tokenInfo);
const usdValue = (tokenInfo?.price * amount) ?? 0;
return {
name: tokenInfo?.name || mint,
symbol: tokenInfo?.symbol || mint,
usdValue,
logo: tokenInfo?.image || DEFAULT_IMAGE_URL,
cid: null,
collectionName: tokenInfo?.name || mint,
collectionLogo: tokenInfo?.image || DEFAULT_IMAGE_URL,
isNft: false
};
}
// Fetch token accounts
export async function fetchTokenAccounts(publicKey: PublicKey) {
// console.log("Fetching token accounts for:", publicKey.toString());
return withRetry(() =>
rpcLimiter.schedule(() =>
connection.getParsedTokenAccountsByOwner(publicKey, {
programId: TOKEN_PROGRAM_ID_ADDRESS,
})
)
);
}
// Fetch token data
export async function handleTokenData(publicKey: PublicKey, tokenAccount: any): Promise<TokenData | null> {
const mintAddress = tokenAccount.account.data.parsed.info.mint;
const amount = tokenAccount.account.data.parsed.info.tokenAmount.uiAmount || 0;
const decimals = tokenAccount.account.data.parsed.info.tokenAmount.decimals;
const [tokenAccountAddress] = PublicKey.findProgramAddressSync(
[publicKey.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), new PublicKey(mintAddress).toBuffer()],
ASSOCIATED_TOKEN_PROGRAM_ID
);
// console.log("πŸ“₯ Fetching Jupiter price...");
let jupiterPrice = null;
try {
const jupiterResponse = await withTokenRetry(
() => apiLimiter.schedule(() => fetchJupiterSwap(mintAddress)),
mintAddress
);
if (jupiterResponse?.skipped) {
console.log(`Skipping price fetch for token ${mintAddress}: ${jupiterResponse.error}`);
} else if (jupiterResponse?.error) {
console.warn(`Jupiter price fetch error for ${mintAddress}:`, jupiterResponse.error);
} else if (jupiterResponse?.data) {
jupiterPrice = jupiterResponse.data;
console.log("πŸ’° Jupiter price data:", jupiterPrice);
}
} catch (error) {
console.warn(`Unable to fetch Jupiter price for token ${mintAddress}:`, error);
}
// console.log("πŸ“₯ Fetching token metadata...");
const metadata = await withTokenRetry(
() => fetchTokenMetadata(new PublicKey(mintAddress), mintAddress, amount),
mintAddress
);
console.log("πŸ“‹ Token metadata:", metadata);
if (!metadata) {
console.log(`Skipping token ${mintAddress} due to metadata fetch failure`);
return null;
}
let price = 0;
let priceSource = 'none';
if (metadata.isNft) {
try {
const floorPrice = await withTokenRetry(
() => apiLimiter.schedule(() => fetchFloorPrice(mintAddress)),
mintAddress
);
price = floorPrice?.usdValue || 0;
priceSource = price > 0 ? 'floor' : 'none';
console.log(`${metadata.collectionName} NFT Floor price: $${price}`);
} catch (error) {
console.warn(`Unable to fetch floor price for NFT ${mintAddress}:`, error);
}
} else {
try {
const tokenPrice = jupiterPrice?.[mintAddress]?.price;
if (tokenPrice) {
price = tokenPrice;
priceSource = 'jupiter';
} else {
price = 0;
priceSource = 'none';
console.log(`No valid price found for token ${mintAddress}: ${jupiterPrice?.[mintAddress]?.price}`);
}
} catch (error) {
console.warn(`Error accessing Jupiter price data for ${mintAddress}:`, error);
price = 0;
priceSource = 'none';
}
}
const usdValue = amount * price;
console.log(`Token ${metadata.name} (${mintAddress}) - Price: $${price} (${priceSource}), Total Value: $${usdValue}`);
// Check if token is swappable with retry
let isSwappable = false;
try {
isSwappable = await withTokenRetry(
() => isTokenSwappable(
mintAddress,
LOCKIN_MINT,
tokenAccount.account.data.parsed.info.tokenAmount.amount
),
mintAddress
) ?? false;
} catch (error) {
console.warn(`Error checking if token ${mintAddress} is swappable:`, error);
}
return {
mintAddress,
tokenAddress: tokenAccountAddress.toString(),
amount,
decimals,
usdValue,
...metadata,
swappable: isSwappable,
};
}
export const deserializeInstruction = (instruction: Instruction) => {
return new TransactionInstruction({
programId: new PublicKey(instruction.programId),
keys: instruction.accounts.map((key: { pubkey: PublicKeyInitData; isSigner: any; isWritable: any; }) => ({
pubkey: new PublicKey(key.pubkey),
isSigner: key.isSigner,
isWritable: key.isWritable,
})),
data: Buffer.from(instruction.data, "base64"),
});
};
export const getAddressLookupTableAccounts = async (connection: Connection, keys: any[]) => {
const addressLookupTableAccountInfos = await connection.getMultipleAccountsInfo(
keys.map((key) => new PublicKey(key))
);
return addressLookupTableAccountInfos.reduce<AddressLookupTableAccount[]>((acc, accountInfo, index) => {
const addressLookupTableAddress = keys[index];
if (accountInfo) {
const addressLookupTableAccount = new AddressLookupTableAccount({
key: new PublicKey(addressLookupTableAddress),
state: AddressLookupTableAccount.deserialize(accountInfo.data as any),
});
if (typeof addressLookupTableAccount !== "undefined") {
acc.push(addressLookupTableAccount);
}
}
return acc;
}, []);
};
export async function isTokenSwappable(inputMint: string, targetMint: string, amount: number): Promise<boolean> {
if (inputMint === LOCKIN_MINT) {
// console.log("βœ… Token is LOCKIN_MINT, automatically swappable");
return true;
}
if (amount === 0) {
// console.log("⚠️ Amount is 0, skipping swappable check");
return false;
}
try {
// console.log("πŸ” Checking if token is swappable:", {
// inputMint,
// targetMint,
// amount
// });
const jupiterQuoteApi = createJupiterApiClient();
return await withRetry(async () => {
// console.log("πŸ“₯ Fetching Jupiter quote...");
const params: QuoteGetRequest = {
inputMint,
outputMint: targetMint,
amount,
slippageBps: 50,
onlyDirectRoutes: false,
};
const quote = await jupiterQuoteApi.quoteGet(params) as any;
return quote?.routes?.length > 0;
}) ?? false;
} catch (error) {
console.error("❌ Error checking if token is swappable:", error);
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment