helpers.ts 8.3 KB

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