solana-kite
Version:
The modern Solana framework for TypeScript.
1,314 lines (1,293 loc) • 49.4 kB
JavaScript
// src/lib/connect.ts
import {
createDefaultRpcTransport,
createSolanaRpcFromTransport as createSolanaRpcFromTransport5,
createSolanaRpcSubscriptions as createSolanaRpcSubscriptions2,
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/keypair.ts
import {
createKeyPairSignerFromBytes,
createKeyPairSignerFromPrivateKeyBytes
} from "@solana/kit";
import { assertKeyGenerationIsAvailable } from "@solana/assertions";
// src/lib/constants.ts
import { lamports, address as toAddress } from "@solana/kit";
var NOT_FOUND = -1;
var TOKEN_PROGRAM = toAddress("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
var TOKEN_EXTENSIONS_PROGRAM = toAddress("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
var ASSOCIATED_TOKEN_PROGRAM = toAddress("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
var SOL = 1000000000n;
var SECONDS = 1e3;
var DEFAULT_AIRDROP_AMOUNT = lamports(1n * SOL);
var DEFAULT_MINIMUM_BALANCE = lamports(500000000n);
var DEFAULT_ENV_KEYPAIR_VARIABLE_NAME = "PRIVATE_KEY";
var DEFAULT_TRANSACTION_RETRIES = 4;
var DEFAULT_TRANSACTION_TIMEOUT = 15 * SECONDS;
var KEYPAIR_LENGTH = 64;
var KEYPAIR_PUBLIC_KEY_OFFSET = 32;
var DEFAULT_FILEPATH = "~/.config/solana/id.json";
var BASE58_CHARACTER_SET = /^[1-9A-HJ-NP-Za-km-z]+$/;
var PKCS_8_PREFIX = new Uint8Array([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
var PKCS_8_PREFIX_LENGTH = PKCS_8_PREFIX.length;
var GRIND_COMPLEXITY_THRESHOLD = 5;
// src/lib/crypto.ts
import { getBase58Decoder, getBase58Encoder } from "@solana/kit";
var exportKey = crypto.subtle.exportKey.bind(crypto.subtle);
var modInverse = (a, m) => {
let [old_r, r] = [a, m];
let [old_s, s] = [1n, 0n];
let [old_t, t] = [0n, 1n];
while (r !== 0n) {
const quotient = old_r / r;
[old_r, r] = [r, old_r - quotient * r];
[old_s, s] = [s, old_s - quotient * s];
[old_t, t] = [t, old_t - quotient * t];
}
return old_s < 0n ? old_s + m : old_s;
};
var P = 2n ** 255n - 19n;
var D = -121665n * modInverse(121666n, P) % P;
var exportRawPrivateKeyBytes = async (privateKey) => {
if (!privateKey.extractable) {
throw new Error("Private key is not extractable");
}
const pkcs8Bytes = await exportKey("pkcs8", privateKey);
const rawPrivateKeyBytes = pkcs8Bytes.slice(PKCS_8_PREFIX_LENGTH);
return new Uint8Array(rawPrivateKeyBytes);
};
var exportRawPublicKeyBytes = async (publicKey) => {
const rawPublicKeyBytes = await exportKey("raw", publicKey);
return new Uint8Array(rawPublicKeyBytes);
};
var getBase58AddressFromPublicKey = async (publicKey) => {
const base58Decoder = getBase58Decoder();
const publicKeyBytes = await exportRawPublicKeyBytes(publicKey);
const publicKeyString = base58Decoder.decode(publicKeyBytes);
return publicKeyString;
};
// src/lib/keypair.ts
var ALLOW_EXTRACTABLE_PRIVATE_KEY_MESSAGE = "yes I understand the risk of extractable private keys and will delete this keypair shortly after saving it to a file";
var grindKeyPair = async (options) => {
await assertKeyGenerationIsAvailable();
const allowExtractablePrivateKey = options.isPrivateKeyExtractable === ALLOW_EXTRACTABLE_PRIVATE_KEY_MESSAGE;
if (options.prefix && !BASE58_CHARACTER_SET.test(options.prefix)) {
throw new Error("Prefix must contain only base58 characters.");
}
if (options.suffix && !BASE58_CHARACTER_SET.test(options.suffix)) {
throw new Error("Suffix must contain only base58 characters.");
}
const keypairGrindComplexity = (options.prefix?.length || 0) + (options.suffix?.length || 0);
if (keypairGrindComplexity > GRIND_COMPLEXITY_THRESHOLD) {
console.warn(
`Generating a keyPair with a prefix and suffix of ${keypairGrindComplexity} characters, this may take some time.`
);
}
let counter = 0;
while (true) {
counter++;
if (!options.silenceGrindProgress && counter % 1e5 === 0) {
console.log(`Keypair grind tries: ${counter}`);
}
const keyPair = await crypto.subtle.generateKey(
// Algorithm. Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20
"Ed25519",
// Allows the private key to be exported (eg for saving it to a file) - public key is always extractable see https://wicg.github.io/webcrypto-secure-curves/#ed25519-operations
allowExtractablePrivateKey,
// Allowed uses
["sign", "verify"]
);
const publicKeyString = await getBase58AddressFromPublicKey(keyPair.publicKey);
if (!options.prefix && !options.suffix) {
return keyPair;
}
const matchesPrefix = options.prefix ? publicKeyString.startsWith(options.prefix) : true;
const matchesSuffix = options.suffix ? publicKeyString.endsWith(options.suffix) : true;
if (matchesPrefix && matchesSuffix) {
return keyPair;
}
continue;
}
};
var createJSONFromKeyPairSigner = async (keyPairSigner) => {
const rawPrivateKeyBytes = await exportRawPrivateKeyBytes(keyPairSigner.keyPair.privateKey);
const rawPublicKeyBytes = await exportRawPublicKeyBytes(keyPairSigner.keyPair.publicKey);
const combinedUint8Array = new Uint8Array(KEYPAIR_LENGTH);
combinedUint8Array.set(new Uint8Array(rawPrivateKeyBytes), 0);
combinedUint8Array.set(new Uint8Array(rawPublicKeyBytes), KEYPAIR_PUBLIC_KEY_OFFSET);
return JSON.stringify(Array.from(combinedUint8Array));
};
var loadWalletFromFile = async (filepath) => {
const path = await import("node:path");
const { readFile } = await import("node:fs/promises");
if (!filepath) {
filepath = DEFAULT_FILEPATH;
}
if (filepath[0] === "~") {
const home = process.env.HOME || null;
if (home) {
filepath = path.join(home, filepath.slice(1));
}
}
let fileContents;
try {
const fileContentsBuffer = await readFile(filepath);
fileContents = fileContentsBuffer.toString();
} catch (thrownObject) {
throw new Error(`Could not read keyPair from file at '${filepath}'`);
}
try {
const parsedFileContents = Uint8Array.from(JSON.parse(fileContents));
return createKeyPairSignerFromBytes(parsedFileContents);
} catch (thrownObject) {
const error = thrownObject;
if (!error.message.includes("Unexpected token")) {
throw error;
}
throw new Error(`Invalid secret key file at '${filepath}'!`);
}
};
var loadWalletFromEnvironment = (variableName) => {
const privateKeyString = process.env[variableName];
if (!privateKeyString) {
throw new Error(`Please set '${variableName}' in environment.`);
}
try {
let decodedPrivateKey = Uint8Array.from(JSON.parse(privateKeyString));
return createKeyPairSignerFromBytes(decodedPrivateKey);
} catch (error) {
throw new Error(`Invalid private key in environment variable '${variableName}'!`);
}
};
var addKeyPairSignerToEnvFile = async (keyPairSigner, variableName, envFileName) => {
const { appendFile } = await import("node:fs/promises");
if (!envFileName) {
envFileName = ".env";
}
const existingSecretKey = process.env[variableName];
if (existingSecretKey) {
throw new Error(`'${variableName}' already exists in env file.`);
}
const privateKeyString = await createJSONFromKeyPairSigner(keyPairSigner);
await appendFile(envFileName, `
# Solana Address: ${keyPairSigner.address}
${variableName}=${privateKeyString}`);
};
var checkAddressMatchesPrivateKey = async (address3, privateKey) => {
const temporarySigner = await createKeyPairSignerFromPrivateKeyBytes(privateKey);
return temporarySigner.address === address3;
};
// 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
}
},
// 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,
assertIsTransactionMessageWithSingleSendingSigner,
createTransactionMessage,
getBase58Decoder as getBase58Decoder2,
getBase58Encoder as getBase58Encoder2,
getSignatureFromTransaction,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signAndSendTransactionMessageWithSigners,
signTransactionMessageWithSigners
} from "@solana/kit";
// src/lib/smart-transactions.ts
import {
getComputeUnitEstimateForTransactionMessageFactory,
appendTransactionMessageInstruction,
isWritableRole,
isInstructionWithData,
SOLANA_ERROR__TRANSACTION_ERROR__ALREADY_PROCESSED,
isSolanaError
} from "@solana/kit";
import {
getSetComputeUnitPriceInstruction,
identifyComputeBudgetInstruction,
COMPUTE_BUDGET_PROGRAM_ADDRESS,
ComputeBudgetInstruction
} from "@solana-program/compute-budget";
// node_modules/@solana/promises/dist/index.node.mjs
function isObject(value) {
return value !== null && (typeof value === "object" || typeof value === "function");
}
function addRaceContender(contender) {
const deferreds = /* @__PURE__ */ new Set();
const record = { deferreds, settled: false };
Promise.resolve(contender).then(
(value) => {
for (const { resolve } of deferreds) {
resolve(value);
}
deferreds.clear();
record.settled = true;
},
(err) => {
for (const { reject } of deferreds) {
reject(err);
}
deferreds.clear();
record.settled = true;
}
);
return record;
}
var wm = /* @__PURE__ */ new WeakMap();
async function safeRace(contenders) {
let deferred;
const result = new Promise((resolve, reject) => {
deferred = { reject, resolve };
for (const contender of contenders) {
if (!isObject(contender)) {
Promise.resolve(contender).then(resolve, reject);
continue;
}
let record = wm.get(contender);
if (record === void 0) {
record = addRaceContender(contender);
record.deferreds.add(deferred);
wm.set(contender, record);
} else if (record.settled) {
Promise.resolve(contender).then(resolve, reject);
} else {
record.deferreds.add(deferred);
}
}
});
return await result.finally(() => {
for (const contender of contenders) {
if (isObject(contender)) {
const record = wm.get(contender);
record.deferreds.delete(deferred);
}
}
});
}
function getAbortablePromise(promise, abortSignal) {
if (!abortSignal) {
return promise;
} else {
return safeRace([
// This promise only ever rejects if the signal is aborted. Otherwise it idles forever.
// It's important that this come before the input promise; in the event of an abort, we
// want to throw even if the input promise's result is ready
new Promise((_, reject) => {
if (abortSignal.aborted) {
reject(abortSignal.reason);
} else {
abortSignal.addEventListener("abort", function() {
reject(this.reason);
});
}
}),
promise
]);
}
}
// src/lib/smart-transactions.ts
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 computeUnitEstimateFn = getComputeUnitEstimateForTransactionMessageFactory({ rpc });
return computeUnitEstimateFn(transactionMessageToSimulate, {
abortSignal: abortSignal ?? void 0
});
};
var sendTransactionWithRetries = async (sendAndConfirmTransaction, transaction, options = {
maximumClientSideRetries: DEFAULT_TRANSACTION_RETRIES,
abortSignal: null,
commitment: "confirmed"
}) => {
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
};
while (retriesLeft) {
try {
const txPromise = sendAndConfirmTransaction(transaction, transactionOptions);
await getAbortablePromise(txPromise, AbortSignal.timeout(DEFAULT_TRANSACTION_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 {
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 getBase58Decoder2().decode(signatureBytes);
};
var signatureBase58StringToBytes = (base58String) => {
return new Uint8Array(getBase58Encoder2().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
)
);
assertIsTransactionMessageWithSingleSendingSigner(transactionMessage);
const signatureBytes = await signAndSendTransactionMessageWithSigners(transactionMessage);
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
}) => {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send({ abortSignal });
let transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(message) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, 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 signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);
try {
if (maximumClientSideRetries) {
await sendTransactionWithRetries(sendAndConfirmTransaction, signedTransaction, {
maximumClientSideRetries,
abortSignal,
commitment
});
} else {
await sendAndConfirmTransaction(signedTransaction, {
commitment,
skipPreflight
});
}
} catch (thrownObject) {
const error = thrownObject;
const transaction = await rpc.getTransaction(signature, {
commitment,
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 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,
airdropAmount = DEFAULT_AIRDROP_AMOUNT
} = options;
let keyPairSigner;
if (envFileName) {
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);
await addKeyPairSignerToEnvFile(temporaryExtractableKeyPairSigner, envVariableName, envFileName);
dotenv.config({ path: envFileName });
keyPairSigner = await loadWalletFromEnvironment(envVariableName);
} else {
const keyPair = await grindKeyPair({
prefix,
suffix
});
keyPairSigner = await createSignerFromKeyPair(keyPair);
}
if (airdropAmount) {
await airdropIfRequired(keyPairSigner.address, airdropAmount, airdropAmount);
}
return keyPairSigner;
};
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, some } from "@solana/kit";
import {
extension as getExtensionData,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getInitializeMetadataPointerInstruction,
getInitializeMintInstruction,
getInitializeTokenMetadataInstruction,
getMintSize,
getMintToInstruction,
getUpdateTokenMetadataFieldInstruction,
tokenMetadataField,
getTransferCheckedInstruction,
fetchMint,
getCreateAssociatedTokenInstruction
} from "@solana-program/token-2022";
import { getCreateAccountInstruction, getTransferSolInstruction } from "@solana-program/system";
var transferLamportsFactory = (sendTransactionFromInstructions) => {
const transferLamports = async ({
source,
destination,
amount,
skipPreflight = true,
maximumClientSideRetries = 0,
abortSignal = null
}) => {
const instruction = getTransferSolInstruction({
amount,
destination,
source
});
const signature = await sendTransactionFromInstructions({
feePayer: source,
instructions: [instruction],
commitment: "confirmed",
skipPreflight,
maximumClientSideRetries,
abortSignal
});
return signature;
};
return transferLamports;
};
var transferTokensFactory = (getMint, sendTransactionFromInstructions) => {
const transferTokens = async ({
sender,
destination,
mintAddress,
amount,
maximumClientSideRetries = 0,
abortSignal = null
}) => {
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, true);
const destinationAssociatedTokenAddress = await getTokenAccountAddress(destination, mintAddress, true);
const createAssociatedTokenInstruction = getCreateAssociatedTokenInstruction({
ata: destinationAssociatedTokenAddress,
mint: mintAddress,
owner: destination,
payer: sender
});
const transferInstruction = getTransferCheckedInstruction({
source: sourceAssociatedTokenAddress,
mint: mintAddress,
destination: destinationAssociatedTokenAddress,
authority: sender.address,
amount,
decimals
});
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 [address3] = await findAssociatedTokenPda({
mint,
owner: wallet,
tokenProgram
});
return address3;
};
var createTokenMintFactory = (rpc, sendTransactionFromInstructions) => {
const createTokenMint = async ({
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 updateTokenMetadataInstruction = getUpdateTokenMetadataFieldInstruction({
metadata: mint.address,
updateAuthority: mintAuthority,
field: tokenMetadataField("Key", ["description"]),
value: "Only Possible On Solana"
});
const instructions = [
createAccountInstruction,
initializeMetadataPointerInstruction,
initializeMintInstruction,
initializeTokenMetadataInstruction,
updateTokenMetadataInstruction
];
await sendTransactionFromInstructions({
feePayer: mintAuthority,
instructions
});
return mint.address;
};
return createTokenMint;
};
var mintTokensFactory = (sendTransactionFromInstructions) => {
const mintTokens = async (mintAddress, mintAuthority, amount, destination) => {
const createAtaInstruction = await getCreateAssociatedTokenInstructionAsync({
payer: mintAuthority,
mint: mintAddress,
owner: destination
});
const associatedTokenAddress = await getTokenAccountAddress(destination, mintAddress, true);
const mintToInstruction = getMintToInstruction({
mint: mintAddress,
token: associatedTokenAddress,
mintAuthority: mintAuthority.address,
amount
});
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 (!options.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;
};
// src/lib/explorer.ts
var getExplorerLinkFactory = (clusterNameOrURL) => {
const getExplorerLink = (linkType, id) => {
const searchParams = {};
if (KNOWN_CLUSTER_NAMES.includes(clusterNameOrURL)) {
const clusterDetails = CLUSTERS[clusterNameOrURL];
if (!clusterDetails.features.isExplorerDefault) {
if (clusterDetails.features.isNameKnownToSolanaExplorer) {
searchParams["cluster"] = clusterNameOrURL;
} else {
searchParams["cluster"] = "custom";
}
if (clusterNameOrURL !== "localnet") {
if (clusterDetails.requiredParam) {
const requiredParamEnvironmentVariable = clusterDetails.requiredParamEnvironmentVariable;
if (!requiredParamEnvironmentVariable) {
throw new Error(`Required param environment variable is not set for cluster ${clusterNameOrURL}`);
}
if (!process.env[requiredParamEnvironmentVariable]) {
throw new Error(`Environment variable '${requiredParamEnvironmentVariable}' is not set.`);
}
const apiKey = process.env[requiredParamEnvironmentVariable];
const params = new URLSearchParams({
[clusterDetails.requiredParam]: apiKey
});
const urlWithParams = `${clusterDetails.httpURL}?${params.toString()}`;
searchParams["customUrl"] = urlWithParams;
} else if (!clusterDetails.httpURL) {
throw new Error(
`Please set either httpUrl or requiredParam for cluster ${clusterNameOrURL} in clusters.ts`
);
} else if (!clusterDetails.features.isNameKnownToSolanaExplorer) {
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 getLamportBalanceFactory = (rpc) => {
const getLamportBalance = async (address3, commitment = "finalized") => {
const getLamportBalanceResponse = await rpc.getBalance(address3, { commitment }).send();
return getLamportBalanceResponse.value;
};
return getLamportBalance;
};
var airdropIfRequiredFactory = (rpc, rpcSubscriptions) => {
const getLamportBalance = getLamportBalanceFactory(rpc);
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
const airdropIfRequired = async (address3, airdropAmount, minimumBalance, commitment = "finalized") => {
if (airdropAmount < 0n) {
throw new Error(`Airdrop amount must be a positive number, not ${airdropAmount}`);
}
if (minimumBalance === 0n) {
const signature2 = await airdrop({
commitment,
recipientAddress: address3,
lamports: airdropAmount
});
return signature2;
}
const balance = await getLamportBalance(address3, commitment);
if (balance >= minimumBalance) {
return null;
}
const signature = await airdrop({
commitment,
recipientAddress: address3,
lamports: airdropAmount
});
return signature;
};
return airdropIfRequired;
};
// src/lib/pdas.ts
import { getAddressEncoder, getProgramDerivedAddress } from "@solana/kit";
var addressEncoder = getAddressEncoder();
var bigIntToSeed = (bigInt, byteLength) => {
const bytes = new Uint8Array(byteLength);
for (let i = 0; i < byteLength && bigInt > 0n; i++) {
bytes[i] = Number(bigInt & 0xffn);
bigInt >>= 8n;
}
return bytes;
};
var getPDAAndBump = async (programAddress, seeds) => {
const seedsUint8Array = seeds.map((seed) => {
if (seed instanceof Uint8Array) {
return seed;
}
if (typeof seed === "bigint") {
return bigIntToSeed(seed, 8);
}
try {
const encoded = addressEncoder.encode(seed);
return encoded;
} catch {
return new TextEncoder().encode(seed);
}
});
const [pda, bump] = await getProgramDerivedAddress({
seeds: seedsUint8Array,
programAddress
});
return { pda, bump };
};
// src/lib/accounts.ts
import {
decodeAccount,
parseBase64RpcAccount,
getBase58Decoder as getBase58Decoder3
} from "@solana/kit";
var getAccountsFactoryFactory = (rpc) => {
const getAccountsFactory = (programAddress, discriminator, decoder) => {
return async () => {
const base58Decoder = getBase58Decoder3();
const getProgramAccountsResults = await rpc.getProgramAccounts(programAddress, {
encoding: "jsonParsed",
filters: [
{
memcmp: {
offset: 0,
bytes: base58Decoder.decode(discriminator)
}
}
]
}).send();
const encodedAccounts = getProgramAccountsResults.map((result) => {
const account = parseBase64RpcAccount(result.pubkey, result.account);
return {
...account,
data: Uint8Array.from(account.data),
exists: true
};
});
const decodedAccounts = encodedAccounts.map((maybeAccount) => {
return decodeAccount(maybeAccount, decoder);
});
return decodedAccounts;
};
};
return getAccountsFactory;
};
// src/lib/messages.ts
import { getBase58Decoder as getBase58Decoder4 } from "@solana/kit";
var signMessageFromWalletApp = async (message, messageSigner) => {
const base58Decoder = getBase58Decoder4();
const encodedMessage = new TextEncoder().encode(message);
const results = await messageSigner.modifyAndSignMessages([
{
content: encodedMessage,
signatures: {}
}
]);
const result = results[0];
const signature = Object.values(result?.signatures)[0];
if (!signature) {
throw new Error("Could not find signature in the result");
}
return base58Decoder.decode(signature);
};
// src/lib/connect.ts
function getWebsocketUrlFromHTTPUrl(httpUrl) {
try {
const url = new URL(httpUrl);
if (url.protocol === "http:") {
url.protocol = "ws:";
} else if (url.protocol === "https:") {
url.protocol = "wss:";
} else {
throw new Error("URL must start with http:// or https://");
}
return url.toString();
} catch (thrownObject) {
throw new Error(`Invalid HTTP URL: ${httpUrl}`);
}
}
var connect = (clusterNameOrURLOrRpc = "localnet", clusterWebSocketURLOrRpcSubscriptions = null) => {
let rpc;
let rpcSubscriptions;
let supportsGetPriorityFeeEstimate = false;
let needsPriorityFees = false;
let enableClientSideRetries = false;
let clusterNameOrURL;
if (typeof clusterNameOrURLOrRpc !== "string") {
rpc = clusterNameOrURLOrRpc;
if (!clusterWebSocketURLOrRpcSubscriptions || typeof clusterWebSocketURLOrRpcSubscriptions === "string") {
throw new Error("When providing an RPC client, you must also provide an RPC subscriptions client");
}
rpcSubscriptions = clusterWebSocketURLOrRpcSubscriptions;
clusterNameOrURL = "custom";
} else {
clusterNameOrURL = clusterNameOrURLOrRpc;
if (clusterNameOrURL === "mainnet") {
clusterNameOrURL = "mainnet-beta";
}
if (KNOWN_CLUSTER_NAMES.includes(clusterNameOrURL)) {
const clusterDetails = CLUSTERS[clusterNameOrURL];
if (clusterDetails.features.supportsGetPriorityFeeEstimate) {
supportsGetPriorityFeeEstimate = true;
}
if (clusterDetails.features.needsPriorityFees) {
needsPriorityFees = true;
}
enableClientSideRetries = clusterDetails.features.enableClientSideRetries;
let httpURL;
let webSocketURL;
if (clusterDetails.httpURL === null || clusterDetails.webSocketURL === null) {
if (!clusterDetails.requiredRpcEnvironmentVariable) {
throw new Error(`Cluster ${clusterNameOrURL} has null URLs but no requiredRpcEnvironmentVariable specified.`);
}
const rpcEndpoint = process.env[clusterDetails.requiredRpcEnvironmentVariable];
if (!rpcEndpoint) {
throw new Error(`Environment variable ${clusterDetails.requiredRpcEnvironmentVariable} is not set.`);
}
httpURL = rpcEndpoint;
webSocketURL = getWebsocketUrlFromHTTPUrl(rpcEndpoint);
} else if (clusterDetails.requiredParamEnvironmentVariable) {
const requiredParamEnvironmentVariable = process.env[clusterDetails.requiredParamEnvironmentVariable];
if (!requiredParamEnvironmentVariable) {
throw new Error(`Environment variable ${clusterDetails.requiredParamEnvironmentVariable} is not set.`);
}
const queryParamsString = new URLSearchParams({
"api-key": requiredParamEnvironmentVariable
});
httpURL = `${clusterDetails.httpURL}?${queryParamsString}`;
webSocketURL = `${clusterDetails.webSocketURL}?${queryParamsString}`;
} else {
httpURL = clusterDetails.httpURL;
webSocketURL = clusterDetails.webSocketURL;
}
const transport = createDefaultRpcTransport({
url: httpURL
});
rpc = createSolanaRpcFromTransport5(transport);
rpcSubscriptions = createSolanaRpcSubscriptions2(webSocketURL);
} else {
if (!clusterWebSocketURLOrRpcSubscriptions || typeof clusterWebSocketURLOrRpcSubscriptions !== "string") {
throw new Error(
`Missing clusterWebSocketURL. Either provide a valid cluster name (${KNOWN_CLUSTER_NAMES_STRING}) or two valid URLs.`
);
}
if (checkIsValidURL(clusterNameOrURL) && checkIsValidURL(clusterWebSocketURLOrRpcSubscriptions)) {
const transport = createDefaultRpcTransport({
url: clusterNameOrURL
});
rpc = createSolanaRpcFromTransport5(transport);
rpcSubscriptions = createSolanaRpcSubscriptions2(clusterWebSocketURLOrRpcSubscriptions);
} else {
throw new Error(
`Unsupported cluster name (valid options are ${KNOWN_CLUSTER_NAMES_STRING}) or URL: ${clusterNameOrURL}. `
);
}
}
}
const typedRpcSubscriptions = rpcSubscriptions;
const sendAndConfirmTransaction = clusterNameOrURL.includes("mainnet") ? sendAndConfirmTransactionFactory3({ rpc, rpcSubscriptions: typedRpcSubscriptions }) : clusterNameOrURL.includes("testnet") ? sendAndConfirmTransactionFactory3({ rpc, rpcSubscriptions: typedRpcSubscriptions }) : sendAndConfirmTransactionFactory3({ rpc, rpcSubscriptions: typedRpcSubscriptions });
const getRecentSignatureConfirmation = clusterNameOrURL.includes("mainnet") ? createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions: typedRpcSubscriptions }) : clusterNameOrURL.includes("testnet") ? createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions: typedRpcSubscriptions }) : createRecentSignatureConfirmationPromiseFactory({ rpc, rpcSubscriptions: typedRpcSubscriptions });
const airdropIfRequired = airdropIfRequiredFactory(rpc, typedRpcSubscriptions);
const createWallet = createWalletFactory(airdropIfRequired);
const createWallets = createWalletsFactory(createWallet);
const getLogs = getLogsFactory(rpc);
const sendTransactionFromInstructions = sendTransactionFromInstructionsFactory(
rpc,
needsPriorityFees,
supportsGetPriorityFeeEstimate,
enableClientSideRetries,
sendAndConfirmTransaction
);
const transferLamports = transferLamportsFactory(sendTransactionFromInstructions);
const createTokenMint = createTokenMintFactory(rpc, sendTransactionFromInstructions);
const getMint = getMintFactory(rpc);
const transferTokens = transferTokensFactory(getMint, sendTransactionFromInstructions);
const mintTokens = mintTokensFactory(sendTransactionFromInstructions);
const getTokenAccountBalance = getTokenAccountBalanceFactory(rpc);
const checkTokenAccountIsClosed = checkTokenAccountIsClosedFactory(getTokenAccountBalance);
const getAccountsFactory = getAccountsFactoryFactory(rpc);
return {
rpc,
rpcSubscriptions,
sendAndConfirmTransaction,
sendTransactionFromInstructions,
getLamportBalance: getLamportBalanceFactory(rpc),
getExplorerLink: getExplorerLinkFactory(clusterNameOrURL),
airdropIfRequired,
createWallet,
createWallets,
getLogs,
getRecentSignatureConfirmation,
transferLamports,
transferTokens,
createTokenMint,
mintTokens,
getTokenAccountAddress,
loadWalletFromFile,
loadWalletFromEnvironment,
getMint,
getTokenAccountBalance,
getPDAAndBump,
checkTokenAccountIsClosed,
getAccountsFactory,
signatureBytesToBase58String,
signatureBase58StringToBytes,
sendTransactionFromInstructionsWithWalletApp: sendTransactionFromInstructionsWithWalletAppFactory(rpc),
signMessageFromWalletApp,
checkAddressMatchesPrivateKey
};
};
export {
ASSOCIATED_TOKEN_PROGRAM,
SOL,
TOKEN_EXTENSIONS_PROGRAM,
TOKEN_PROGRAM,
connect
};
//# sourceMappingURL=index.js.map