solana-kite
Version:
The modern Solana framework for TypeScript.
1,416 lines (1,402 loc) • 70.4 kB
JavaScript
import {
ASSOCIATED_TOKEN_PROGRAM,
DEFAULT_AIRDROP_AMOUNT,
DEFAULT_ENV_KEYPAIR_VARIABLE_NAME,
DEFAULT_TRANSACTION_RETRIES,
DEFAULT_TRANSACTION_TIMEOUT,
DISCRIMINATOR_SIZE,
LENGTH_FIELD_SIZE,
NOT_FOUND,
PUBLIC_KEY_SIZE,
SECONDS,
SOL,
TOKEN_EXTENSIONS_PROGRAM,
TOKEN_PROGRAM,
addKeyPairSignerToEnvFile,
checkAddressMatchesPrivateKey,
checkIfAddressIsPublicKey,
createJSONFromKeyPairSigner,
grindKeyPair,
loadWalletFromEnvironment,
loadWalletFromFile
} from "./chunk-D5BJ3YJK.js";
// src/lib/connect.ts
import {
createDefaultRpcTransport,
createSolanaRpcFromTransport as createSolanaRpcFromTransport5,
createSolanaRpcSubscriptions as createSolanaRpcSubscriptions3,
sendAndConfirmTransactionFactory as sendAndConfirmTransactionFactory3
} from "@solana/kit";
import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation";
// src/lib/url.ts
var encodeURL = (baseUrl, searchParams) => {
const url = new URL(baseUrl);
url.search = new URLSearchParams(searchParams).toString();
return url.toString();
};
var checkIsValidURL = (string) => {
try {
new URL(string);
return true;
} catch (error) {
return false;
}
};
// src/lib/clusters.ts
var CLUSTERS = {
// Solana Labs RPCs
// Don't add a seperate entry for 'mainnet'. Instead, we'll correct the cluster name to 'mainnet-beta'
// in the connect function, and avoid making a duplicate entry.
// Note these are rate-limited public RPCs, you should use a commercial RPC provider for production apps.
"mainnet-beta": {
httpURL: "https://api.mainnet-beta.solana.com",
webSocketURL: "wss://api.mainnet-beta.solana.com",
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: true,
isNameKnownToSolanaExplorer: true,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
testnet: {
httpURL: "https://api.testnet.solana.com",
webSocketURL: "wss://api.testnet.solana.com",
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: true,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
devnet: {
httpURL: "https://api.devnet.solana.com",
webSocketURL: "wss://api.devnet.solana.com",
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: true,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
// Quicknode RPCs
"quicknode-mainnet": {
// These endpoints are configured at https://dashboard.quicknode.com/endpoints
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "QUICKNODE_SOLANA_MAINNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
// TODO: add support for QuickNode priority fee API
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
"quicknode-devnet": {
// These endpoints are configured at https://dashboard.quicknode.com/endpoints
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "QUICKNODE_SOLANA_DEVNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
// TODO: add support for QuickNode priority fee API
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
"quicknode-testnet": {
// These endpoints are configured at https://dashboard.quicknode.com/endpoints
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "QUICKNODE_SOLANA_TESTNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
// TODO: add support for QuickNode priority fee API
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
// Helius RPCs
"helius-mainnet": {
httpURL: "https://mainnet.helius-rpc.com/",
webSocketURL: "wss://mainnet.helius-rpc.com/",
requiredParam: "api-key",
requiredParamEnvironmentVariable: "HELIUS_API_KEY",
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: true,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
"helius-devnet": {
httpURL: "https://devnet.helius-rpc.com/",
webSocketURL: "wss://devnet.helius-rpc.com/",
requiredParam: "api-key",
requiredParamEnvironmentVariable: "HELIUS_API_KEY",
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
// Triton RPCs
"triton-mainnet": {
// These endpoints are configured at https://triton.one/
// Format: https://your-app.mainnet.rpcpool.com/your-token
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "TRITON_SOLANA_MAINNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
"triton-devnet": {
// These endpoints are configured at https://triton.one/
// Format: https://your-app.devnet.rpcpool.com/your-token
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "TRITON_SOLANA_DEVNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
"triton-testnet": {
// These endpoints are configured at https://triton.one/
// Format: https://your-app.testnet.rpcpool.com/your-token
httpURL: null,
webSocketURL: null,
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: "TRITON_SOLANA_TESTNET_ENDPOINT",
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: true,
needsPriorityFees: true
}
},
// Localnet
localnet: {
httpURL: "http://localhost:8899",
webSocketURL: "ws://localhost:8900",
requiredParam: null,
requiredParamEnvironmentVariable: null,
requiredRpcEnvironmentVariable: null,
features: {
isExplorerDefault: false,
isNameKnownToSolanaExplorer: false,
supportsGetPriorityFeeEstimate: false,
enableClientSideRetries: false,
needsPriorityFees: false
}
}
};
var KNOWN_CLUSTER_NAMES = Object.keys(CLUSTERS);
var KNOWN_CLUSTER_NAMES_STRING = KNOWN_CLUSTER_NAMES.join(", ");
// src/lib/transactions.ts
import {
appendTransactionMessageInstructions,
assertIsTransactionWithinSizeLimit as assertIsTransactionWithinSizeLimit2,
createTransactionMessage,
getBase58Decoder,
getBase58Encoder,
getSignatureFromTransaction,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
assertIsTransactionMessageWithBlockhashLifetime
} from "@solana/kit";
// src/lib/smart-transactions.ts
import {
appendTransactionMessageInstruction,
isWritableRole,
isInstructionWithData,
SOLANA_ERROR__TRANSACTION_ERROR__ALREADY_PROCESSED,
isSolanaError,
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
assertIsTransactionWithinSizeLimit
} from "@solana/kit";
import {
getSetComputeUnitPriceInstruction,
identifyComputeBudgetInstruction,
COMPUTE_BUDGET_PROGRAM_ADDRESS,
ComputeBudgetInstruction,
estimateComputeUnitLimitFactory
} from "@solana-program/compute-budget";
import { getAbortablePromise } from "@solana/promises";
var getPriorityFeeEstimate = async (rpc, supportsGetPriorityFeeEstimate, transactionMessage, abortSignal = null) => {
const accountKeys = [
.../* @__PURE__ */ new Set([
...transactionMessage.instructions.flatMap(
(instruction) => (instruction.accounts ?? []).filter((account) => isWritableRole(account.role)).map((account) => account.address)
)
])
];
if (!supportsGetPriorityFeeEstimate) {
const recentFeesResponse = await rpc.getRecentPrioritizationFees([...accountKeys]).send({ abortSignal });
const recentFeesValues = recentFeesResponse.reduce((accumulator, current) => {
if (current.prioritizationFee > 0n) {
return [...accumulator, current.prioritizationFee];
} else {
return accumulator;
}
}, []);
recentFeesValues.sort((a, b) => Number(a - b));
return Number(recentFeesValues[Math.floor(recentFeesValues.length / 2)]);
}
const { priorityFeeEstimate } = await rpc.getPriorityFeeEstimate({
accountKeys,
options: {
// See https://docs.helius.dev/solana-apis/priority-fee-api
// Per Evan at Helius 20250213: recommended: true is not longer preferred,
// instead use priorityLevel: "High"
priorityLevel: "High"
}
}).send({ abortSignal });
return priorityFeeEstimate;
};
var getComputeUnitEstimate = async (rpc, transactionMessage, abortSignal = null) => {
const hasExistingComputeBudgetPriceInstruction = transactionMessage.instructions.some(
(instruction) => instruction.programAddress === COMPUTE_BUDGET_PROGRAM_ADDRESS && isInstructionWithData(instruction) && identifyComputeBudgetInstruction(instruction) === ComputeBudgetInstruction.SetComputeUnitPrice
);
const transactionMessageToSimulate = hasExistingComputeBudgetPriceInstruction ? transactionMessage : appendTransactionMessageInstruction(getSetComputeUnitPriceInstruction({ microLamports: 0n }), transactionMessage);
const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc });
return estimateComputeUnitLimit(transactionMessageToSimulate, {
abortSignal: abortSignal ?? void 0
});
};
var sendTransactionWithRetries = async (sendAndConfirmTransaction, transaction, options = {
maximumClientSideRetries: DEFAULT_TRANSACTION_RETRIES,
abortSignal: null,
commitment: "confirmed",
timeout: null
}) => {
if (options.commitment === "finalized") {
console.warn(
"Using finalized commitment for transaction with retries is not recommended. This will likely result in blockhash expiration."
);
}
let retriesLeft = options.maximumClientSideRetries;
const transactionOptions = {
// TODO: web3.js wants explicit undefineds. Fix upstream.
abortSignal: options.abortSignal || void 0,
commitment: options.commitment,
// This is the server-side retries and should always be 0.
// We will do retries here on the client.
// See https://docs.helius.dev/solana-rpc-nodes/sending-transactions-on-solana#sending-transactions-without-the-sdk
maxRetries: 0n
};
let timeout;
if (options.timeout) {
timeout = options.timeout;
} else {
switch (options.commitment) {
case "processed":
timeout = 5 * SECONDS;
break;
case "confirmed":
timeout = 15 * SECONDS;
break;
case "finalized":
timeout = 30 * SECONDS;
break;
default:
timeout = DEFAULT_TRANSACTION_TIMEOUT;
break;
}
}
while (retriesLeft) {
try {
assertIsTransactionWithinSizeLimit(transaction);
const txPromise = sendAndConfirmTransaction(transaction, transactionOptions);
await getAbortablePromise(txPromise, AbortSignal.timeout(timeout));
break;
} catch (error) {
if (error instanceof DOMException && error.name === "TimeoutError") {
console.debug("Transaction not confirmed, retrying...");
} else if (isSolanaError(error, SOLANA_ERROR__TRANSACTION_ERROR__ALREADY_PROCESSED)) {
break;
} else if (isSolanaError(error, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {
if (error.cause && isSolanaError(error.cause, SOLANA_ERROR__TRANSACTION_ERROR__ALREADY_PROCESSED)) {
break;
}
} else {
throw error;
}
} finally {
retriesLeft--;
}
}
};
// src/lib/transactions.ts
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction as getSetComputeUnitPriceInstruction2 } from "@solana-program/compute-budget";
// src/lib/logs.ts
var getLogsFactory = (rpc) => {
const getLogs = async (signature) => {
const transaction = await rpc.getTransaction(signature, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0
}).send();
if (!transaction?.meta) {
throw new Error(`Transaction not found: ${signature}`);
}
return transaction.meta.logMessages ?? [];
};
return getLogs;
};
var getErrorMessageFromLogs = (logMessages) => {
const anchorErrorIndex = logMessages.findIndex(
(logMessage) => logMessage.includes("Program log: AnchorError caused by account:")
);
if (anchorErrorIndex !== NOT_FOUND) {
const programInvokeLog = logMessages[anchorErrorIndex - 2];
const programName = programInvokeLog?.split("Program ")[1]?.split(" invoke")[0];
const instructionHandlerLog = logMessages[anchorErrorIndex - 1];
const instructionHandlerName = instructionHandlerLog?.split("Instruction: ")[1];
const errorMessage = logMessages[anchorErrorIndex].split("Error Message: ")[1]?.split(".")[0]?.trim();
if (!errorMessage || !programName || !instructionHandlerName) {
return null;
}
return `${programName}.${instructionHandlerName}: ${errorMessage}`;
}
const errorIndex = logMessages.findIndex((logMessage) => logMessage.includes("Program log: Error: "));
if (errorIndex !== NOT_FOUND) {
const programInvokeLog = logMessages[errorIndex - 2];
const programName = programInvokeLog?.split("Program ")[1]?.split(" invoke")[0];
const instructionHandlerLog = logMessages[errorIndex - 1];
const instructionHandlerName = instructionHandlerLog?.split("Instruction: ")[1];
const errorMessage = logMessages[errorIndex].split("Program log: Error: ")[1]?.trim();
if (!errorMessage || !programName || !instructionHandlerName) {
return null;
}
return `${programName}.${instructionHandlerName}: ${errorMessage}`;
}
const systemErrorIndex = logMessages.findIndex(
(logMessage) => logMessage.includes(": ") && !logMessage.includes("Program log:") && !logMessage.includes("Program ") && !logMessage.includes(" consumed ") && !logMessage.includes(" success") && !logMessage.includes(" failed:")
);
if (systemErrorIndex !== NOT_FOUND) {
const programInvokeLog = logMessages[systemErrorIndex - 1];
const programName = programInvokeLog?.split("Program ")[1]?.split(" invoke")[0];
const [instructionName, ...errorParts] = logMessages[systemErrorIndex].split(": ");
const errorMessage = errorParts.join(": ");
if (!errorMessage || !programName || !instructionName) {
return null;
}
const cleanErrorMessage = errorMessage.replace(/Address {[^}]*}/, "").replace(/\s+/g, " ").trim();
return `${programName}.${instructionName}: ${cleanErrorMessage}`;
}
return null;
};
// src/lib/transactions.ts
var signatureBytesToBase58String = (signatureBytes) => {
return getBase58Decoder().decode(signatureBytes);
};
var signatureBase58StringToBytes = (base58String) => {
return new Uint8Array(getBase58Encoder().encode(base58String));
};
var sendTransactionFromInstructionsWithWalletAppFactory = (rpc) => {
const sendTransactionFromInstructionsWithWalletApp = async ({
feePayer,
instructions,
abortSignal = null
}) => {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send({ abortSignal });
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(message) => setTransactionMessageFeePayerSigner(feePayer, message),
(message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message),
(message) => appendTransactionMessageInstructions(instructions, message)
);
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signatureBytes = await rpc.sendTransaction(signedTransaction, {
encoding: "base64",
skipPreflight: false
}).send({ abortSignal });
const signature = signatureBytesToBase58String(signatureBytes);
return signature;
};
return sendTransactionFromInstructionsWithWalletApp;
};
var sendTransactionFromInstructionsFactory = (rpc, needsPriorityFees, supportsGetPriorityFeeEstimate, enableClientSideRetries, sendAndConfirmTransaction) => {
const sendTransactionFromInstructions = async ({
feePayer,
instructions,
commitment = "confirmed",
skipPreflight = true,
maximumClientSideRetries = enableClientSideRetries ? DEFAULT_TRANSACTION_RETRIES : 0,
abortSignal = null,
timeout
}) => {
let placeholderBlockhash = {
blockhash: "11111111111111111111111111111111",
lastValidBlockHeight: 0n
};
let transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(message) => setTransactionMessageLifetimeUsingBlockhash(placeholderBlockhash, message),
(message) => setTransactionMessageFeePayerSigner(feePayer, message),
(message) => appendTransactionMessageInstructions(instructions, message)
);
if (needsPriorityFees) {
const [priorityFeeEstimate, computeUnitEstimate] = await Promise.all([
getPriorityFeeEstimate(rpc, supportsGetPriorityFeeEstimate, transactionMessage, abortSignal),
getComputeUnitEstimate(rpc, transactionMessage, abortSignal)
]);
const setComputeUnitPriceInstruction = getSetComputeUnitPriceInstruction2({
microLamports: BigInt(priorityFeeEstimate)
});
const setComputeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: Math.ceil(computeUnitEstimate * 1.1)
});
transactionMessage = appendTransactionMessageInstructions(
[setComputeUnitPriceInstruction, setComputeUnitLimitInstruction],
transactionMessage
);
}
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send({ abortSignal });
transactionMessage = setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, transactionMessage);
assertIsTransactionMessageWithBlockhashLifetime(transactionMessage);
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithinSizeLimit2(signedTransaction);
const blockhashSignedTransaction = signedTransaction;
const signature = getSignatureFromTransaction(blockhashSignedTransaction);
try {
if (maximumClientSideRetries) {
await sendTransactionWithRetries(sendAndConfirmTransaction, blockhashSignedTransaction, {
maximumClientSideRetries,
abortSignal,
commitment,
timeout
});
} else {
await sendAndConfirmTransaction(blockhashSignedTransaction, {
commitment,
skipPreflight
});
}
} catch (thrownObject) {
const error = thrownObject;
const commitmentToUse = commitment === "processed" ? "confirmed" : commitment;
const transaction = await rpc.getTransaction(signature, {
commitment: commitmentToUse,
maxSupportedTransactionVersion: 0
}).send();
error.transaction = transaction;
if (error.message.includes("custom program error") && transaction.meta?.logMessages) {
const betterMessage = getErrorMessageFromLogs(transaction.meta.logMessages);
if (betterMessage) {
error.message = betterMessage;
}
}
throw error;
}
return signature;
};
return sendTransactionFromInstructions;
};
// src/lib/wallets.ts
import { createSignerFromKeyPair } from "@solana/kit";
import dotenv from "dotenv";
var expandHomeDirectoryPath = async (filePath) => {
if (filePath[0] === "~") {
const path = await import("path");
const home = process.env.HOME || null;
if (home) {
return path.join(home, filePath.slice(1));
}
}
return filePath;
};
var ensureFileDoesNotExist = async (filePath) => {
const { access } = await import("fs/promises");
try {
await access(filePath);
throw new Error(`File '${filePath}' already exists.`);
} catch (error) {
const fileError = error;
if (fileError.message.includes("already exists")) {
throw fileError;
}
if (fileError.code !== "ENOENT") {
throw fileError;
}
}
};
var createWalletFactory = (airdropIfRequired) => {
const createWallet = async (options = {}) => {
if (options.envVariableName && !options.envFileName) {
options.envFileName = ".env";
}
const {
prefix = null,
suffix = null,
envFileName = null,
envVariableName = DEFAULT_ENV_KEYPAIR_VARIABLE_NAME,
fileName = null,
airdropAmount = DEFAULT_AIRDROP_AMOUNT,
commitment = null
} = options;
if (envFileName && fileName) {
throw new Error("Cannot save to both envFileName and fileName. Please specify only one.");
}
if (fileName) {
const filePath = await expandHomeDirectoryPath(fileName);
await ensureFileDoesNotExist(filePath);
}
let keyPairSigner;
if (envFileName || fileName) {
const temporaryExtractableKeyPair = await grindKeyPair({
prefix,
suffix,
silenceGrindProgress: false,
isPrivateKeyExtractable: "yes I understand the risk of extractable private keys and will delete this keypair shortly after saving it to a file"
});
const temporaryExtractableKeyPairSigner = await createSignerFromKeyPair(temporaryExtractableKeyPair);
if (envFileName) {
await addKeyPairSignerToEnvFile(temporaryExtractableKeyPairSigner, envVariableName, envFileName);
dotenv.config({ path: envFileName });
keyPairSigner = await loadWalletFromEnvironment(envVariableName);
}
if (fileName) {
const { writeFile } = await import("fs/promises");
const filePath = await expandHomeDirectoryPath(fileName);
const privateKeyJSON = await createJSONFromKeyPairSigner(temporaryExtractableKeyPairSigner);
await writeFile(filePath, privateKeyJSON);
const { loadWalletFromFile: loadWalletFromFile2 } = await import("./keypair-63XDBLMZ.js");
keyPairSigner = await loadWalletFromFile2(filePath);
}
} else {
const keyPair = await grindKeyPair({
prefix,
suffix
});
keyPairSigner = await createSignerFromKeyPair(keyPair);
}
const finalKeyPairSigner = keyPairSigner;
if (airdropAmount) {
await airdropIfRequired(finalKeyPairSigner.address, airdropAmount, airdropAmount, commitment);
}
return finalKeyPairSigner;
};
return createWallet;
};
var createWalletsFactory = (createWallet) => {
const createWallets = (amount, options = {}) => {
const walletPromises = Array.from({ length: amount }, () => createWallet(options));
return Promise.all(walletPromises);
};
return createWallets;
};
// src/lib/tokens.ts
import {
generateKeyPairSigner,
isSolanaError as isSolanaError2,
some
} from "@solana/kit";
import {
extension as getExtensionData,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getInitializeMetadataPointerInstruction,
getInitializeMintInstruction,
getInitializeTokenMetadataInstruction,
getMintSize,
getMintToInstruction,
getUpdateTokenMetadataFieldInstruction,
tokenMetadataField,
getTransferCheckedInstruction,
fetchMint,
getCreateAssociatedTokenInstruction,
getBurnCheckedInstruction as getBurnCheckedInstructionToken2022,
getCloseAccountInstruction as getCloseAccountInstructionToken2022
} from "@solana-program/token-2022";
import { getCreateAccountInstruction, getTransferSolInstruction } from "@solana-program/system";
// src/lib/errors.ts
var ensureError = (thrownObject) => {
if (thrownObject instanceof Error) {
return thrownObject;
}
return new Error(`Non-Error thrown: ${String(thrownObject)}`);
};
// src/lib/tokens.ts
var transferLamportsFactory = (sendTransactionFromInstructions) => {
const transferLamports = async ({
source,
destination,
amount,
commitment = "confirmed",
skipPreflight = true,
maximumClientSideRetries = 0,
abortSignal = null
}) => {
const instruction = getTransferSolInstruction({
amount,
destination,
source
});
const signature = await sendTransactionFromInstructions({
feePayer: source,
instructions: [instruction],
commitment,
skipPreflight,
maximumClientSideRetries,
abortSignal
});
return signature;
};
return transferLamports;
};
var transferTokensFactory = (getMint, sendTransactionFromInstructions) => {
const transferTokens = async ({
sender,
destination,
mintAddress,
amount,
maximumClientSideRetries = 0,
abortSignal = null,
useTokenExtensions = true
}) => {
const tokenProgram = useTokenExtensions ? TOKEN_EXTENSIONS_PROGRAM : TOKEN_PROGRAM;
const mint = await getMint(mintAddress);
if (!mint) {
throw new Error(`Mint not found: ${mintAddress}`);
}
const decimals = mint.data.decimals;
const sourceAssociatedTokenAddress = await getTokenAccountAddress(sender.address, mintAddress, useTokenExtensions);
const destinationAssociatedTokenAddress = await getTokenAccountAddress(
destination,
mintAddress,
useTokenExtensions
);
const createAssociatedTokenInstruction = getCreateAssociatedTokenInstruction({
ata: destinationAssociatedTokenAddress,
mint: mintAddress,
owner: destination,
payer: sender,
tokenProgram
});
const transferInstruction = getTransferCheckedInstruction(
{
source: sourceAssociatedTokenAddress,
mint: mintAddress,
destination: destinationAssociatedTokenAddress,
authority: sender.address,
amount,
decimals
},
{ programAddress: tokenProgram }
);
const signature = await sendTransactionFromInstructions({
feePayer: sender,
instructions: [createAssociatedTokenInstruction, transferInstruction],
commitment: "confirmed",
skipPreflight: true,
maximumClientSideRetries,
abortSignal
});
return signature;
};
return transferTokens;
};
var getTokenAccountAddress = async (wallet, mint, useTokenExtensions = false) => {
const tokenProgram = useTokenExtensions ? TOKEN_EXTENSIONS_PROGRAM : TOKEN_PROGRAM;
const [address] = await findAssociatedTokenPda({
mint,
owner: wallet,
tokenProgram
});
return address;
};
var createClassicTokenMint = async ({
rpc,
sendTransactionFromInstructions,
mintAuthority,
decimals
}) => {
const { getInitializeMintInstruction: getClassicInitializeMintInstruction, getMintSize: getClassicMintSize } = await import("@solana-program/token");
const mint = await generateKeyPairSigner();
const mintSpace = BigInt(getClassicMintSize());
const rent = await rpc.getMinimumBalanceForRentExemption(mintSpace).send();
const createAccountInstruction = getCreateAccountInstruction({
payer: mintAuthority,
newAccount: mint,
lamports: rent,
space: mintSpace,
programAddress: TOKEN_PROGRAM
});
const initializeMintInstruction = getClassicInitializeMintInstruction(
{
mint: mint.address,
decimals,
mintAuthority: mintAuthority.address
},
{ programAddress: TOKEN_PROGRAM }
);
const instructions = [createAccountInstruction, initializeMintInstruction];
await sendTransactionFromInstructions({
feePayer: mintAuthority,
instructions
});
return mint.address;
};
var createTokenExtensionsMint = async ({
rpc,
sendTransactionFromInstructions,
mintAuthority,
decimals,
name,
symbol,
uri,
additionalMetadata = {}
}) => {
const mint = await generateKeyPairSigner();
const additionalMetadataMap = additionalMetadata instanceof Map ? additionalMetadata : new Map(Object.entries(additionalMetadata));
const metadataPointerExtensionData = getExtensionData("MetadataPointer", {
authority: some(mintAuthority.address),
metadataAddress: some(mint.address)
});
const tokenMetadataExtensionData = getExtensionData("TokenMetadata", {
updateAuthority: some(mintAuthority.address),
mint: mint.address,
name,
symbol,
uri,
additionalMetadata: additionalMetadataMap
});
const spaceWithoutMetadata = BigInt(getMintSize([metadataPointerExtensionData]));
const spaceWithMetadata = BigInt(getMintSize([metadataPointerExtensionData, tokenMetadataExtensionData]));
const rent = await rpc.getMinimumBalanceForRentExemption(spaceWithMetadata).send();
const createAccountInstruction = getCreateAccountInstruction({
payer: mintAuthority,
newAccount: mint,
lamports: rent,
space: spaceWithoutMetadata,
programAddress: TOKEN_EXTENSIONS_PROGRAM
});
const initializeMetadataPointerInstruction = getInitializeMetadataPointerInstruction({
mint: mint.address,
authority: mintAuthority.address,
metadataAddress: mint.address
});
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals,
mintAuthority: mintAuthority.address
});
const initializeTokenMetadataInstruction = getInitializeTokenMetadataInstruction({
metadata: mint.address,
updateAuthority: mintAuthority.address,
mint: mint.address,
mintAuthority,
name: tokenMetadataExtensionData.name,
symbol: tokenMetadataExtensionData.symbol,
uri: tokenMetadataExtensionData.uri
});
const updateInstructions = Array.from(additionalMetadataMap.entries()).map(([key, value]) => {
return getUpdateTokenMetadataFieldInstruction({
metadata: mint.address,
updateAuthority: mintAuthority,
field: tokenMetadataField("Key", [key]),
value
});
});
const instructions = [
createAccountInstruction,
initializeMetadataPointerInstruction,
initializeMintInstruction,
initializeTokenMetadataInstruction,
...updateInstructions
];
await sendTransactionFromInstructions({
feePayer: mintAuthority,
instructions
});
return mint.address;
};
var createTokenMintFactory = (rpc, sendTransactionFromInstructions) => {
const createTokenMint = async ({
mintAuthority,
decimals,
name,
symbol,
uri,
additionalMetadata = {},
useTokenExtensions = true
}) => {
if (useTokenExtensions) {
if (!name || !symbol || !uri) {
throw new Error("name, symbol, and uri are required when useTokenExtensions is true");
}
return createTokenExtensionsMint({
rpc,
sendTransactionFromInstructions,
mintAuthority,
decimals,
name,
symbol,
uri,
additionalMetadata
});
}
return createClassicTokenMint({
rpc,
sendTransactionFromInstructions,
mintAuthority,
decimals
});
};
return createTokenMint;
};
var mintTokensFactory = (sendTransactionFromInstructions) => {
const mintTokens = async (mintAddress, mintAuthority, amount, destination, useTokenExtensions = true) => {
const tokenProgram = useTokenExtensions ? TOKEN_EXTENSIONS_PROGRAM : TOKEN_PROGRAM;
const createAtaInstruction = await getCreateAssociatedTokenInstructionAsync({
payer: mintAuthority,
mint: mintAddress,
owner: destination,
tokenProgram
});
const associatedTokenAddress = await getTokenAccountAddress(destination, mintAddress, useTokenExtensions);
const mintToInstruction = getMintToInstruction(
{
mint: mintAddress,
token: associatedTokenAddress,
mintAuthority: mintAuthority.address,
amount
},
{ programAddress: tokenProgram }
);
const transactionSignature = await sendTransactionFromInstructions({
feePayer: mintAuthority,
instructions: [createAtaInstruction, mintToInstruction]
});
return transactionSignature;
};
return mintTokens;
};
var getMintFactory = (rpc) => {
const getMint = async (mintAddress, commitment = "confirmed") => {
const mint = await fetchMint(rpc, mintAddress, { commitment });
return mint;
};
return getMint;
};
var getTokenAccountBalanceFactory = (rpc) => {
const getTokenAccountBalance = async (options) => {
const { wallet, mint, tokenAccount, useTokenExtensions } = options;
if (!tokenAccount) {
if (!wallet || !mint) {
throw new Error("wallet and mint are required when tokenAccount is not provided");
}
options.tokenAccount = await getTokenAccountAddress(wallet, mint, useTokenExtensions);
}
const result = await rpc.getTokenAccountBalance(options.tokenAccount).send();
const { amount, decimals, uiAmount, uiAmountString } = result.value;
return {
amount: BigInt(amount),
decimals,
uiAmount,
uiAmountString
};
};
return getTokenAccountBalance;
};
var checkTokenAccountIsClosedFactory = (getTokenAccountBalance) => {
const checkTokenAccountIsClosed = async (options) => {
try {
await getTokenAccountBalance(options);
return false;
} catch (thrownObject) {
const error = thrownObject;
if (error.message.includes("Invalid param: could not find account")) {
return true;
}
throw error;
}
};
return checkTokenAccountIsClosed;
};
var getTokenMetadataFactory = (rpc) => {
const getTokenMetadata = async (mintAddress, commitment = "confirmed") => {
let mint;
try {
mint = await fetchMint(rpc, mintAddress, { commitment });
} catch (error) {
if (isSolanaError2(error)) {
throw error;
}
throw new Error(
`Mint not found: ${mintAddress}. Original error: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
if (!mint) {
throw new Error(`Mint not found: ${mintAddress}`);
}
const extensions = mint.data?.extensions?.__option === "Some" ? mint.data.extensions.value : [];
const metadataPointerExtension = extensions.find((extension) => extension.__kind === "MetadataPointer");
if (!metadataPointerExtension || metadataPointerExtension.metadataAddress.__option === "None") {
throw new Error(`No metadata pointer extension found for mint: ${mintAddress}`);
}
const metadataAddress = metadataPointerExtension.metadataAddress?.__option === "Some" ? metadataPointerExtension.metadataAddress.value : null;
if (!metadataAddress) {
throw new Error(`No metadata address found in metadata pointer extension for mint: ${mintAddress}`);
}
if (metadataAddress.toString() === mintAddress.toString()) {
const tokenMetadata = extensions.find((extension) => extension.__kind === "TokenMetadata");
if (!tokenMetadata) {
throw new Error(`TokenMetadata extension not found in mint account: ${mintAddress}`);
}
const additionalMetadata = {};
if (tokenMetadata.additionalMetadata instanceof Map) {
for (const [key, value] of tokenMetadata.additionalMetadata) {
additionalMetadata[key] = value;
}
}
const updateAuthority = tokenMetadata.updateAuthority?.__option === "Some" ? tokenMetadata.updateAuthority.value : null;
return {
updateAuthority,
mint: tokenMetadata.mint,
name: tokenMetadata.name,
symbol: tokenMetadata.symbol,
uri: tokenMetadata.uri,
additionalMetadata
};
} else {
const metadataAccount = await rpc.getAccountInfo(metadataAddress, { commitment }).send();
if (!metadataAccount.value) {
throw new Error(`Metadata account not found: ${metadataAddress}`);
}
const data = metadataAccount.value.data;
return parseTokenMetadataAccount(data);
}
};
const parseTokenMetadataAccount = (data) => {
let offset = DISCRIMINATOR_SIZE;
const updateAuthority = data.slice(offset, offset + PUBLIC_KEY_SIZE);
offset += PUBLIC_KEY_SIZE;
const mintAddressFromMetadata = data.slice(offset, offset + PUBLIC_KEY_SIZE);
offset += PUBLIC_KEY_SIZE;
const nameLength = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const name = new TextDecoder("utf8").decode(data.slice(offset, offset + nameLength));
offset += nameLength;
const symbolLength = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const symbol = new TextDecoder("utf8").decode(data.slice(offset, offset + symbolLength));
offset += symbolLength;
const uriLength = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const uri = new TextDecoder("utf8").decode(data.slice(offset, offset + uriLength));
offset += uriLength;
const additionalMetadataCount = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const additionalMetadata = {};
for (let i = 0; i < additionalMetadataCount; i++) {
const keyLength = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const key = new TextDecoder("utf8").decode(data.slice(offset, offset + keyLength));
offset += keyLength;
const valueLength = new DataView(data.buffer, data.byteOffset).getUint32(offset, true);
offset += LENGTH_FIELD_SIZE;
const value = new TextDecoder("utf8").decode(data.slice(offset, offset + valueLength));
offset += valueLength;
additionalMetadata[key] = value;
}
return {
updateAuthority,
mint: mintAddressFromMetadata,
name,
symbol,
uri,
additionalMetadata
};
};
return getTokenMetadata;
};
var updateTokenMetadataFactory = (rpc, sendTransactionFromInstructions) => {
const updateTokenMetadata = async ({
mintAddress,
updateAuthority,
name,
symbol,
uri,
additionalMetadata,
commitment = "confirmed"
}) => {
const mint = await fetchMint(rpc, mintAddress, { commitment });
if (!mint) {
throw new Error(`Mint not found: ${mintAddress}`);
}
const extensions = mint.data?.extensions?.__option === "Some" ? mint.data.extensions.value : [];
const metadataPointerExtension = extensions.find((extension) => extension.__kind === "MetadataPointer");
if (!metadataPointerExtension || metadataPointerExtension.metadataAddress.__option === "None") {
throw new Error(`No metadata pointer extension found for mint: ${mintAddress}`);
}
const metadataAddress = metadataPointerExtension.metadataAddress?.__option === "Some" ? metadataPointerExtension.metadataAddress.value : null;
if (!metadataAddress) {
throw new Error(`No metadata address found in metadata pointer extension for mint: ${mintAddress}`);
}
const instructions = [];
if (name !== void 0) {
instructions.push(
getUpdateTokenMetadataFieldInstruction({
metadata: metadataAddress,
updateAuthority,
field: tokenMetadataField("Name"),
value: name
})
);
}
if (symbol !== void 0) {
instructions.push(
getUpdateTokenMetadataFieldInstruction({
metadata: metadataAddress,
updateAuthority,
field: tokenMetadataField("Symbol"),
value: symbol
})
);
}
if (uri !== void 0) {
instructions.push(
getUpdateTokenMetadataFieldInstruction({
metadata: metadataAddress,
updateAuthority,
field: tokenMetadataField("Uri"),
value: uri
})
);
}
if (additionalMetadata) {
for (const [key, value] of Object.entries(additionalMetadata)) {
instructions.push(
getUpdateTokenMetadataFieldInstruction({
metadata: metadataAddress,
updateAuthority,
field: tokenMetadataField("Key", [key]),
value
})
);
}
}
if (instructions.length === 0) {
throw new Error("No metadata fields provided to update");
}
const signature = await sendTransactionFromInstructions({
feePayer: updateAuthority,
instructions,
commitment
});
return signature;
};
return updateTokenMetadata;
};
var watchTokenBalanceFactory = (rpc, rpcSubscriptions) => {
const getTokenAccountBalance = getTokenAccountBalanceFactory(rpc);
const watchTokenBalance = (ownerAddress, mintAddress, callback, useTokenExtensions = true) => {
const abortController = new AbortController();
let lastUpdateSlot = -1n;
const fetchInitialBalance = async () => {
try {
const balance = await getTokenAccountBalance({
wallet: ownerAddress,
mint: mintAddress,
useTokenExtensions
});
callback(null, balance);
} catch (error) {
if (error?.name !== "AbortError") {
callback(null, {
amount: 0n,
decimals: 9,
uiAmount: 0,
uiAmountString: "0"
});
}
}
};
const subscribeToUpdates = async () => {
try {
const tokenAccountAddress = await getTokenAccountAddress(ownerAddress, mintAddress, useTokenExtensions);
const accountInfoNotifications = await rpcSubscriptions.accountNotifications(tokenAccountAddress).subscribe({ abortSignal: abortController.signal });
try {
for await (const {
context: { slot }
} of accountInfoNotifications) {
if (slot < lastUpdateSlot) {
continue;
}
lastUpdateSlot = slot;
try {
const balance = await getTokenAccountBalance({
wallet: ownerAddress,
mint: mintAddress,
useTokenExtensions
});
callback(null, balance);
} catch (balanceError) {
if (balanceError?.name !== "AbortError") {
callback(null, {
amount: 0n,
decimals: 9,
uiAmount: 0,
uiAmountString: "0"
});
}
}
}
} catch (thrownObject) {
const error = ensureError(thrownObject);
if (error.name !== "AbortError") {
callback(error, null);
}
}
} catch (thrownObject) {
const error = ensureError(thrownObject);
if (error.name !== "AbortError") {
callback(error, null);
}
}
};
fetchInitialBalance();
subscribeToUpdates();
return () => {
abortController.abort();
};
};
return watchTokenBalance;
};
var burnTokensFactory = (getMint, sendTransactionFromInstructions) => {
const burnTokens = async ({
mintAddress,
owner,
amount,
useTokenExtensions = true,
skipPreflight = true,
maximumClientSideRetries = 0,
abortSignal = null
}) => {
const tokenProgram = useTokenExtensions ? TOKEN_EXTENSIONS_PROGRAM : TOKEN_PROGRAM;
const mint = await getMint(mintAddress);
if (!mint) {
throw new Error(`Mint not found: ${mintAddress}`);
}
const decimals = mint.data.decimals;
const tokenAccount = await getTokenAccountAddress(owner.address, mintAddress, useTokenExtensions);
let burnInstruction;
if (useTokenExtensions) {
burnInstruction = getBurnCheckedInstructionToken2022({
account: tokenAccount,
mint: mintAddress,
authority: owner,
amount,
decimals
});
} else {
const { getBurnCheckedInstruction: getBurnCheckedInstructionClassic } = await import("@solana-program/token");
burnInstruction = getBurnCheckedInstructionClassic(
{
account: tokenAccount,
mint: mintAddress,
authority: owner,
amount,
decimals
},
{ programAddress: tokenProgram }
);
}
const signature = await sendTransactionFromInstructions({
feePayer: owner,
instructions: [burnInstruction],
commitment: "confirmed",
skipPreflight,
maximumClientSideRetries,
abortSignal
});
return signature;
};
return burnTokens;
};
var closeTokenAccountFactory = (sendTransactionFromInstructions) => {
const closeTokenAccount = async ({
owner,
tokenAccount,
wallet,
mint,
destination,
useTokenExtensions = true,
skipPreflight = true,
maximumClientSideRetries = 0,
abortSignal = null
}) => {
let accountToClose;
if (tokenAccount) {
accountToClose = tokenAccount;
} else if (wallet && mint) {
accountToClose = await getTokenAccountAddress(wallet, mint, useTokenExtensions);
} else {
throw new Error("Either tokenAccount or both wallet and mint must be provided");
}
const rentDestination = destination || owner.address;
let closeInstruction;
if (useTokenExtensions) {
closeInstruction = getCloseAccountInstructionToken2022({
account: accountToClose,
destination: rentDestination,
owner
});
} else {
const { getCloseAccountInstruction: getCloseAccountInstructionClassic } = await import("@solana-program/token");
closeInstruction = getCloseAccountInstructionClassic({
account: accountToClose,
destination: rentDestination,
owner
});
}
const signature = await sendTransactionFromInstructions({
feePayer: owner,
instructions: [closeInstruction],
commitment: "confirmed",
skipPreflight,
maximumClientSideRetries,
abortSignal
});
return signature;
};
return closeTokenAccount;
};
var getTokenAccountsFactory = (rpc) => {
const getTokenAccounts = async (walletAddress, excludeZeroBalance = false) => {
const [classicTokenProgramResponse, tokenExtensionsProgramResponse] = await Promise.all([
rpc.getTokenAccountsByOwner(
walletAddress,
{
programId: TOKEN_PROGRAM
},
{
encoding: "jsonParsed"
}
).send(),
rpc.getTokenAccountsByOwner(
walletAddress,
{
programId: TOKEN_EXTENSIONS_PROGRAM
},
{
encoding: "jsonParsed"
}
).send()
]);
const allAccounts = [...classicTokenProgramResponse.value, ...tokenExtensionsProgramResponse.value];
if (excludeZeroBalance) {
return allAccounts.filter((account) => {
const amount = account.account.data.parsed?.info?.tokenAmount?.amount;
return amount && BigInt(amount) > 0n;
});
}
return allAccounts;
};
return getTokenAccounts;
};
// src/lib/explorer.ts
var getExplorerLinkFactory = (clusterNameOrURL) => {
const getExplorerLink = (linkType, id) => {
const searchParams = {};
if (KNOWN_CLUSTER_NAMES.includes(clusterNameOrURL)) {
const clusterConfig = CLUSTERS[clusterNameOrURL];
const clusterDetails = getClusterDetailsFromClusterConfig(clusterNameOrURL, clusterConfig);
if (!clusterDetails.features.isExplorerDefault) {
if (clusterDetails.features.isNameKnownToSolanaExplorer) {
searchParams["cluster"] = clusterNameOrURL;
} else {
searchParams["cluster"] = "custom";
}
if (!clusterDetails.features.isNameKnownToSolanaExplorer && clusterNameOrURL !== "localnet") {
searchParams["customUrl"] = clusterDetails.httpURL;
}
}
} else {
if (checkIsValidURL(clusterNameOrURL)) {
searchParams["cluster"] = "custom";
searchParams["customUrl"] = clusterNameOrURL;
} else {
throw new Error(`Unsupported cluster name: ${clusterNameOrURL}`);
}
}
let baseUrl = "";
if (linkType === "address") {
baseUrl = `https://explorer.solana.com/address/${id}`;
}
if (linkType === "transaction" || linkType === "tx") {
baseUrl = `https://explorer.solana.com/tx/${id}`;
}
if (linkType === "block") {
baseUrl = `https://explorer.solana.com/block/${id}`;
}
return encodeURL(baseUrl, searchParams);
};
return getExplorerLink;
};
// src/lib/sol.ts
import {
airdropFactory
} from "@solana/kit";
var ensureError2 = (thrownObject) => {
if (thrownObject instanceof Error) {
return thrownObject;
}
return new Error(`Non-Error thrown: ${String(thrownObject)}`);
};
var getLamportBalanceFactory = (rpc) => {
const getLamportBalance = async (address, commitment = "finalized") => {
const getLamportBalanceResponse = await rpc.getBalance(address, { commitment }).send();
return getLamportBalanceResponse.value;
};
return getLamportBalance;
};
var airdropIfRequiredFactory = (rpc, rpcSubscriptions) => {
const getLamportBalance = getLamportBalanceFactory(rpc);
const airdrop = airdropFactory({ rp