UNPKG

solana-kite

Version:

The modern Solana framework for TypeScript.

1,314 lines (1,293 loc) 49.4 kB
// 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