helpers.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import { type Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
  4. // define some default locations
  5. const DEFAULT_KEY_DIR_NAME = '.local_keys';
  6. const DEFAULT_PUBLIC_KEY_FILE = 'keys.json';
  7. const DEFAULT_DEMO_DATA_FILE = 'demo.json';
  8. /*
  9. Load locally stored PublicKey addresses
  10. */
  11. export function loadPublicKeysFromFile(absPath = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`) {
  12. try {
  13. if (!absPath) throw Error('No path provided');
  14. if (!fs.existsSync(absPath)) throw Error('File does not exist.');
  15. // load the public keys from the file
  16. const data = JSON.parse(fs.readFileSync(absPath, { encoding: 'utf-8' })) || {};
  17. // convert all loaded keyed values into valid public keys
  18. for (const [key, value] of Object.entries(data)) {
  19. data[key] = new PublicKey(value as string) ?? '';
  20. }
  21. return data;
  22. } catch (err) {
  23. // console.warn("Unable to load local file");
  24. }
  25. // always return an object
  26. return {};
  27. }
  28. /*
  29. Locally save a demo data to the filesystem for later retrieval
  30. */
  31. export function saveDemoDataToFile(name: string, newData: any, absPath = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_DEMO_DATA_FILE}`) {
  32. try {
  33. let data: object = {};
  34. // fetch all the current values, when the storage file exists
  35. if (fs.existsSync(absPath)) data = JSON.parse(fs.readFileSync(absPath, { encoding: 'utf-8' })) || {};
  36. data = { ...data, [name]: newData };
  37. // actually save the data to the file
  38. fs.writeFileSync(absPath, JSON.stringify(data), {
  39. encoding: 'utf-8',
  40. });
  41. return data;
  42. } catch (err) {
  43. console.warn('Unable to save to file');
  44. // console.warn(err);
  45. }
  46. // always return an object
  47. return {};
  48. }
  49. /*
  50. Locally save a PublicKey addresses to the filesystem for later retrieval
  51. */
  52. export function savePublicKeyToFile(name: string, publicKey: PublicKey, absPath = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`) {
  53. try {
  54. // if (!absPath) throw Error("No path provided");
  55. // if (!fs.existsSync(absPath)) throw Error("File does not exist.");
  56. // fetch all the current values
  57. let data: any = loadPublicKeysFromFile(absPath);
  58. // convert all loaded keyed values from PublicKeys to strings
  59. for (const [key, value] of Object.entries(data)) {
  60. data[key as any] = (value as PublicKey).toBase58();
  61. }
  62. data = { ...data, [name]: publicKey.toBase58() };
  63. // actually save the data to the file
  64. fs.writeFileSync(absPath, JSON.stringify(data), {
  65. encoding: 'utf-8',
  66. });
  67. // reload the keys for sanity
  68. data = loadPublicKeysFromFile(absPath);
  69. return data;
  70. } catch (err) {
  71. console.warn('Unable to save to file');
  72. }
  73. // always return an object
  74. return {};
  75. }
  76. /*
  77. Load a locally stored JSON keypair file and convert it to a valid Keypair
  78. */
  79. export function loadKeypairFromFile(absPath: string) {
  80. try {
  81. if (!absPath) throw Error('No path provided');
  82. if (!fs.existsSync(absPath)) throw Error('File does not exist.');
  83. // load the keypair from the file
  84. const keyfileBytes = JSON.parse(fs.readFileSync(absPath, { encoding: 'utf-8' }));
  85. // parse the loaded secretKey into a valid keypair
  86. const keypair = Keypair.fromSecretKey(new Uint8Array(keyfileBytes));
  87. return keypair;
  88. } catch (err) {
  89. // return false;
  90. throw err;
  91. }
  92. }
  93. /*
  94. Save a locally stored JSON keypair file for later importing
  95. */
  96. export function saveKeypairToFile(keypair: Keypair, fileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
  97. fileName = path.join(dirName, `${fileName}.json`);
  98. // create the `dirName` directory, if it does not exists
  99. if (!fs.existsSync(`./${dirName}/`)) fs.mkdirSync(`./${dirName}/`);
  100. // remove the current file, if it already exists
  101. if (fs.existsSync(fileName)) fs.unlinkSync(fileName);
  102. // write the `secretKey` value as a string
  103. fs.writeFileSync(fileName, `[${keypair.secretKey.toString()}]`, {
  104. encoding: 'utf-8',
  105. });
  106. return fileName;
  107. }
  108. /*
  109. Attempt to load a keypair from the filesystem, or generate and save a new one
  110. */
  111. export function loadOrGenerateKeypair(fileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
  112. try {
  113. // compute the path to locate the file
  114. const searchPath = path.join(dirName, `${fileName}.json`);
  115. let keypair = Keypair.generate();
  116. // attempt to load the keypair from the file
  117. if (fs.existsSync(searchPath)) keypair = loadKeypairFromFile(searchPath);
  118. // when unable to locate the keypair, save the new one
  119. else saveKeypairToFile(keypair, fileName, dirName);
  120. return keypair;
  121. } catch (err) {
  122. console.error('loadOrGenerateKeypair:', err);
  123. throw err;
  124. }
  125. }
  126. /*
  127. Compute the Solana explorer address for the various data
  128. */
  129. export function explorerURL({
  130. address,
  131. txSignature,
  132. cluster,
  133. }: {
  134. address?: string;
  135. txSignature?: string;
  136. cluster?: 'devnet' | 'testnet' | 'mainnet' | 'mainnet-beta';
  137. }) {
  138. let baseUrl: string;
  139. //
  140. if (address) baseUrl = `https://explorer.solana.com/address/${address}`;
  141. else if (txSignature) baseUrl = `https://explorer.solana.com/tx/${txSignature}`;
  142. else return '[unknown]';
  143. // auto append the desired search params
  144. const url = new URL(baseUrl);
  145. url.searchParams.append('cluster', cluster || 'devnet');
  146. return `${url.toString()}\n`;
  147. }
  148. /**
  149. * Auto airdrop the given wallet of of a balance of < 0.5 SOL
  150. */
  151. export async function airdropOnLowBalance(connection: Connection, keypair: Keypair, forceAirdrop = false) {
  152. // get the current balance
  153. const balance = await connection.getBalance(keypair.publicKey);
  154. // define the low balance threshold before airdrop
  155. const MIN_BALANCE_TO_AIRDROP = LAMPORTS_PER_SOL / 2; // current: 0.5 SOL
  156. // check the balance of the two accounts, airdrop when low
  157. if (forceAirdrop === true || balance < MIN_BALANCE_TO_AIRDROP) {
  158. console.log(`Requesting airdrop of 1 SOL to ${keypair.publicKey.toBase58()}...`);
  159. await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL).then((sig) => {
  160. console.log('Tx signature:', sig);
  161. // balance = balance + LAMPORTS_PER_SOL;
  162. });
  163. // fetch the new balance
  164. // const newBalance = await connection.getBalance(keypair.publicKey);
  165. // return newBalance;
  166. }
  167. // else console.log("Balance of:", balance / LAMPORTS_PER_SOL, "SOL");
  168. return balance;
  169. }
  170. /*
  171. Helper function to extract a transaction signature from a failed transaction's error message
  172. */
  173. export async function extractSignatureFromFailedTransaction(connection: Connection, err: any, fetchLogs?: boolean) {
  174. if (err?.signature) return err.signature;
  175. // extract the failed transaction's signature
  176. const failedSig = new RegExp(/^((.*)?Error: )?(Transaction|Signature) ([A-Z0-9]{32,}) /gim).exec(err?.message?.toString())?.[4];
  177. // ensure a signature was found
  178. if (failedSig) {
  179. // when desired, attempt to fetch the program logs from the cluster
  180. if (fetchLogs)
  181. await connection
  182. .getTransaction(failedSig, {
  183. maxSupportedTransactionVersion: 0,
  184. })
  185. .then((tx) => {
  186. console.log(`\n==== Transaction logs for ${failedSig} ====`);
  187. console.log(explorerURL({ txSignature: failedSig }), '');
  188. console.log(tx?.meta?.logMessages ?? 'No log messages provided by RPC');
  189. console.log('==== END LOGS ====\n');
  190. });
  191. else {
  192. console.log('\n========================================');
  193. console.log(explorerURL({ txSignature: failedSig }));
  194. console.log('========================================\n');
  195. }
  196. }
  197. // always return the failed signature value
  198. return failedSig;
  199. }
  200. /*
  201. Standard number formatter
  202. */
  203. export function numberFormatter(num: number, forceDecimals = false) {
  204. // set the significant figures
  205. const minimumFractionDigits = num < 1 || forceDecimals ? 10 : 2;
  206. // do the formatting
  207. return new Intl.NumberFormat(undefined, {
  208. minimumFractionDigits,
  209. }).format(num);
  210. }
  211. /*
  212. Display a separator in the console, with our without a message
  213. */
  214. export function printConsoleSeparator(message?: string) {
  215. console.log('\n===============================================');
  216. console.log('===============================================\n');
  217. if (message) console.log(message);
  218. }