123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- import fs from "fs";
- import path from "path";
- import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
- // define some default locations
- const DEFAULT_KEY_DIR_NAME = ".local_keys";
- const DEFAULT_PUBLIC_KEY_FILE = "keys.json";
- const DEFAULT_DEMO_DATA_FILE = "demo.json";
- /*
- Load locally stored PublicKey addresses
- */
- export function loadPublicKeysFromFile(
- absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`,
- ) {
- try {
- if (!absPath) throw Error("No path provided");
- if (!fs.existsSync(absPath)) throw Error("File does not exist.");
- // load the public keys from the file
- const data = JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" })) || {};
- // convert all loaded keyed values into valid public keys
- for (const [key, value] of Object.entries(data)) {
- data[key] = new PublicKey(value as string) ?? "";
- }
- return data;
- } catch (err) {
- // console.warn("Unable to load local file");
- }
- // always return an object
- return {};
- }
- /*
- Locally save a demo data to the filesystem for later retrieval
- */
- export function saveDemoDataToFile(
- name: string,
- newData: any,
- absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_DEMO_DATA_FILE}`,
- ) {
- try {
- let data: object = {};
- // fetch all the current values, when the storage file exists
- if (fs.existsSync(absPath))
- data = JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" })) || {};
- data = { ...data, [name]: newData };
- // actually save the data to the file
- fs.writeFileSync(absPath, JSON.stringify(data), {
- encoding: "utf-8",
- });
- return data;
- } catch (err) {
- console.warn("Unable to save to file");
- // console.warn(err);
- }
- // always return an object
- return {};
- }
- /*
- Locally save a PublicKey addresses to the filesystem for later retrieval
- */
- export function savePublicKeyToFile(
- name: string,
- publicKey: PublicKey,
- absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`,
- ) {
- try {
- // if (!absPath) throw Error("No path provided");
- // if (!fs.existsSync(absPath)) throw Error("File does not exist.");
- // fetch all the current values
- let data: any = loadPublicKeysFromFile(absPath);
- // convert all loaded keyed values from PublicKeys to strings
- for (const [key, value] of Object.entries(data)) {
- data[key as any] = (value as PublicKey).toBase58();
- }
- data = { ...data, [name]: publicKey.toBase58() };
- // actually save the data to the file
- fs.writeFileSync(absPath, JSON.stringify(data), {
- encoding: "utf-8",
- });
- // reload the keys for sanity
- data = loadPublicKeysFromFile(absPath);
- return data;
- } catch (err) {
- console.warn("Unable to save to file");
- }
- // always return an object
- return {};
- }
- /*
- Load a locally stored JSON keypair file and convert it to a valid Keypair
- */
- export function loadKeypairFromFile(absPath: string) {
- try {
- if (!absPath) throw Error("No path provided");
- if (!fs.existsSync(absPath)) throw Error("File does not exist.");
- // load the keypair from the file
- const keyfileBytes = JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" }));
- // parse the loaded secretKey into a valid keypair
- const keypair = Keypair.fromSecretKey(new Uint8Array(keyfileBytes));
- return keypair;
- } catch (err) {
- // return false;
- throw err;
- }
- }
- /*
- Save a locally stored JSON keypair file for later importing
- */
- export function saveKeypairToFile(
- keypair: Keypair,
- fileName: string,
- dirName: string = DEFAULT_KEY_DIR_NAME,
- ) {
- fileName = path.join(dirName, `${fileName}.json`);
- // create the `dirName` directory, if it does not exists
- if (!fs.existsSync(`./${dirName}/`)) fs.mkdirSync(`./${dirName}/`);
- // remove the current file, if it already exists
- if (fs.existsSync(fileName)) fs.unlinkSync(fileName);
- // write the `secretKey` value as a string
- fs.writeFileSync(fileName, `[${keypair.secretKey.toString()}]`, {
- encoding: "utf-8",
- });
- return fileName;
- }
- /*
- Attempt to load a keypair from the filesystem, or generate and save a new one
- */
- export function loadOrGenerateKeypair(fileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
- try {
- // compute the path to locate the file
- const searchPath = path.join(dirName, `${fileName}.json`);
- let keypair = Keypair.generate();
- // attempt to load the keypair from the file
- if (fs.existsSync(searchPath)) keypair = loadKeypairFromFile(searchPath);
- // when unable to locate the keypair, save the new one
- else saveKeypairToFile(keypair, fileName, dirName);
- return keypair;
- } catch (err) {
- console.error("loadOrGenerateKeypair:", err);
- throw err;
- }
- }
- /*
- Compute the Solana explorer address for the various data
- */
- export function explorerURL({
- address,
- txSignature,
- cluster,
- }: {
- address?: string;
- txSignature?: string;
- cluster?: "devnet" | "testnet" | "mainnet" | "mainnet-beta";
- }) {
- let baseUrl: string;
- //
- if (address) baseUrl = `https://explorer.solana.com/address/${address}`;
- else if (txSignature) baseUrl = `https://explorer.solana.com/tx/${txSignature}`;
- else return "[unknown]";
- // auto append the desired search params
- const url = new URL(baseUrl);
- url.searchParams.append("cluster", cluster || "devnet");
- return url.toString() + "\n";
- }
- /**
- * Auto airdrop the given wallet of of a balance of < 0.5 SOL
- */
- export async function airdropOnLowBalance(
- connection: Connection,
- keypair: Keypair,
- forceAirdrop: boolean = false,
- ) {
- // get the current balance
- let balance = await connection.getBalance(keypair.publicKey);
- // define the low balance threshold before airdrop
- const MIN_BALANCE_TO_AIRDROP = LAMPORTS_PER_SOL / 2; // current: 0.5 SOL
- // check the balance of the two accounts, airdrop when low
- if (forceAirdrop === true || balance < MIN_BALANCE_TO_AIRDROP) {
- console.log(`Requesting airdrop of 1 SOL to ${keypair.publicKey.toBase58()}...`);
- await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL).then(sig => {
- console.log("Tx signature:", sig);
- // balance = balance + LAMPORTS_PER_SOL;
- });
- // fetch the new balance
- // const newBalance = await connection.getBalance(keypair.publicKey);
- // return newBalance;
- }
- // else console.log("Balance of:", balance / LAMPORTS_PER_SOL, "SOL");
- return balance;
- }
- /*
- Helper function to extract a transaction signature from a failed transaction's error message
- */
- export async function extractSignatureFromFailedTransaction(
- connection: Connection,
- err: any,
- fetchLogs?: boolean,
- ) {
- if (err?.signature) return err.signature;
- // extract the failed transaction's signature
- const failedSig = new RegExp(/^((.*)?Error: )?(Transaction|Signature) ([A-Z0-9]{32,}) /gim).exec(
- err?.message?.toString(),
- )?.[4];
- // ensure a signature was found
- if (failedSig) {
- // when desired, attempt to fetch the program logs from the cluster
- if (fetchLogs)
- await connection
- .getTransaction(failedSig, {
- maxSupportedTransactionVersion: 0,
- })
- .then(tx => {
- console.log(`\n==== Transaction logs for ${failedSig} ====`);
- console.log(explorerURL({ txSignature: failedSig }), "");
- console.log(tx?.meta?.logMessages ?? "No log messages provided by RPC");
- console.log(`==== END LOGS ====\n`);
- });
- else {
- console.log("\n========================================");
- console.log(explorerURL({ txSignature: failedSig }));
- console.log("========================================\n");
- }
- }
- // always return the failed signature value
- return failedSig;
- }
- /*
- Standard number formatter
- */
- export function numberFormatter(num: number, forceDecimals = false) {
- // set the significant figures
- const minimumFractionDigits = num < 1 || forceDecimals ? 10 : 2;
- // do the formatting
- return new Intl.NumberFormat(undefined, {
- minimumFractionDigits,
- }).format(num);
- }
- /*
- Display a separator in the console, with our without a message
- */
- export function printConsoleSeparator(message?: string) {
- console.log("\n===============================================");
- console.log("===============================================\n");
- if (message) console.log(message);
- }
|