123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- import fs from 'node:fs';
- import path from 'node:path';
- import { type 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
- TODO: use the helpers library and delete this function
- */
- export function loadPublicKeysFromFile(absPath = `${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 = `${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 = `${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) {
- console.error('loadKeypairFromFile:', err);
- throw err;
- }
- }
- /*
- Save a locally stored JSON keypair file for later importing
- TODO: delete this function and use the helpers library
- */
- export function saveKeypairToFile(keypair: Keypair, relativeFileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
- const fileName = path.join(dirName, `${relativeFileName}.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 = false) {
- // get the current balance
- const 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);
- }
|