helpers.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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. console.error('loadKeypairFromFile:', err);
  90. throw err;
  91. }
  92. }
  93. /*
  94. Save a locally stored JSON keypair file for later importing
  95. TODO: delete this function and use the helpers library
  96. */
  97. export function saveKeypairToFile(keypair: Keypair, relativeFileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
  98. const fileName = path.join(dirName, `${relativeFileName}.json`);
  99. // create the `dirName` directory, if it does not exists
  100. if (!fs.existsSync(`./${dirName}/`)) fs.mkdirSync(`./${dirName}/`);
  101. // remove the current file, if it already exists
  102. if (fs.existsSync(fileName)) fs.unlinkSync(fileName);
  103. // write the `secretKey` value as a string
  104. fs.writeFileSync(fileName, `[${keypair.secretKey.toString()}]`, {
  105. encoding: 'utf-8',
  106. });
  107. return fileName;
  108. }
  109. /*
  110. Attempt to load a keypair from the filesystem, or generate and save a new one
  111. */
  112. export function loadOrGenerateKeypair(fileName: string, dirName: string = DEFAULT_KEY_DIR_NAME) {
  113. try {
  114. // compute the path to locate the file
  115. const searchPath = path.join(dirName, `${fileName}.json`);
  116. let keypair = Keypair.generate();
  117. // attempt to load the keypair from the file
  118. if (fs.existsSync(searchPath)) keypair = loadKeypairFromFile(searchPath);
  119. // when unable to locate the keypair, save the new one
  120. else saveKeypairToFile(keypair, fileName, dirName);
  121. return keypair;
  122. } catch (err) {
  123. console.error('loadOrGenerateKeypair:', err);
  124. throw err;
  125. }
  126. }
  127. /*
  128. Compute the Solana explorer address for the various data
  129. */
  130. export function explorerURL({
  131. address,
  132. txSignature,
  133. cluster,
  134. }: {
  135. address?: string;
  136. txSignature?: string;
  137. cluster?: 'devnet' | 'testnet' | 'mainnet' | 'mainnet-beta';
  138. }) {
  139. let baseUrl: string;
  140. //
  141. if (address) baseUrl = `https://explorer.solana.com/address/${address}`;
  142. else if (txSignature) baseUrl = `https://explorer.solana.com/tx/${txSignature}`;
  143. else return '[unknown]';
  144. // auto append the desired search params
  145. const url = new URL(baseUrl);
  146. url.searchParams.append('cluster', cluster || 'devnet');
  147. return `${url.toString()}\n`;
  148. }
  149. /**
  150. * Auto airdrop the given wallet of of a balance of < 0.5 SOL
  151. */
  152. export async function airdropOnLowBalance(connection: Connection, keypair: Keypair, forceAirdrop = false) {
  153. // get the current balance
  154. const balance = await connection.getBalance(keypair.publicKey);
  155. // define the low balance threshold before airdrop
  156. const MIN_BALANCE_TO_AIRDROP = LAMPORTS_PER_SOL / 2; // current: 0.5 SOL
  157. // check the balance of the two accounts, airdrop when low
  158. if (forceAirdrop === true || balance < MIN_BALANCE_TO_AIRDROP) {
  159. console.log(`Requesting airdrop of 1 SOL to ${keypair.publicKey.toBase58()}...`);
  160. await connection.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL).then((sig) => {
  161. console.log('Tx signature:', sig);
  162. // balance = balance + LAMPORTS_PER_SOL;
  163. });
  164. // fetch the new balance
  165. // const newBalance = await connection.getBalance(keypair.publicKey);
  166. // return newBalance;
  167. }
  168. // else console.log("Balance of:", balance / LAMPORTS_PER_SOL, "SOL");
  169. return balance;
  170. }
  171. /*
  172. Helper function to extract a transaction signature from a failed transaction's error message
  173. */
  174. export async function extractSignatureFromFailedTransaction(connection: Connection, err: any, fetchLogs?: boolean) {
  175. if (err?.signature) return err.signature;
  176. // extract the failed transaction's signature
  177. const failedSig = new RegExp(/^((.*)?Error: )?(Transaction|Signature) ([A-Z0-9]{32,}) /gim).exec(err?.message?.toString())?.[4];
  178. // ensure a signature was found
  179. if (failedSig) {
  180. // when desired, attempt to fetch the program logs from the cluster
  181. if (fetchLogs)
  182. await connection
  183. .getTransaction(failedSig, {
  184. maxSupportedTransactionVersion: 0,
  185. })
  186. .then((tx) => {
  187. console.log(`\n==== Transaction logs for ${failedSig} ====`);
  188. console.log(explorerURL({ txSignature: failedSig }), '');
  189. console.log(tx?.meta?.logMessages ?? 'No log messages provided by RPC');
  190. console.log('==== END LOGS ====\n');
  191. });
  192. else {
  193. console.log('\n========================================');
  194. console.log(explorerURL({ txSignature: failedSig }));
  195. console.log('========================================\n');
  196. }
  197. }
  198. // always return the failed signature value
  199. return failedSig;
  200. }
  201. /*
  202. Standard number formatter
  203. */
  204. export function numberFormatter(num: number, forceDecimals = false) {
  205. // set the significant figures
  206. const minimumFractionDigits = num < 1 || forceDecimals ? 10 : 2;
  207. // do the formatting
  208. return new Intl.NumberFormat(undefined, {
  209. minimumFractionDigits,
  210. }).format(num);
  211. }
  212. /*
  213. Display a separator in the console, with our without a message
  214. */
  215. export function printConsoleSeparator(message?: string) {
  216. console.log('\n===============================================');
  217. console.log('===============================================\n');
  218. if (message) console.log(message);
  219. }