helpers.ts 8.3 KB

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