├── .gitignore ├── .prettierrc ├── package.json ├── tsconfig.json ├── scripts ├── 1.simpleTransaction.ts ├── 2.complexTransaction.ts └── 0.basics.ts ├── utils.ts ├── README.md └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .env.local 4 | local.env 5 | .local_keys 6 | .cache 7 | test-ledger 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": false, 5 | "printWidth": 100, 6 | "trailingComma": "all", 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto", 9 | "proseWrap": "always" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-basics-crash-course", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "type": "commonjs", 6 | "extension": [ 7 | "ts" 8 | ], 9 | "dependencies": { 10 | "@solana-developers/helpers": "^2.8.1", 11 | "@solana/web3.js": "^1.98.0", 12 | "dotenv": "^16.4.7" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^9.22.0", 16 | "ts-node": "^10.9.2", 17 | "tsconfig-paths": "^4.2.0", 18 | "typescript": "^5.8.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "preserveConstEnums": true, 8 | "noEmit": true, 9 | "sourceMap": false, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowJs": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"], 20 | }, 21 | }, 22 | "include": ["**/*.ts", "**/**.ts", "**/*.tsx"], 23 | "exclude": ["node_modules", ".vscode"], 24 | } 25 | -------------------------------------------------------------------------------- /scripts/1.simpleTransaction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Introduction to the Solana web3.js package 3 | * Demonstrating how to build and send simple transactions to the blockchain 4 | */ 5 | 6 | import * as dotenv from "dotenv"; 7 | import { 8 | Connection, 9 | Keypair, 10 | LAMPORTS_PER_SOL, 11 | SystemProgram, 12 | TransactionMessage, 13 | VersionedTransaction, 14 | clusterApiUrl, 15 | } from "@solana/web3.js"; 16 | import { getExplorerLink, initializeKeypair } from "@solana-developers/helpers"; 17 | import { KEYPAIR_PAYER_ENV_NAME } from "@/utils"; 18 | 19 | dotenv.config(); 20 | 21 | ////////////////////////////////////////////////////////////////////////////// 22 | ////////////////////////////////////////////////////////////////////////////// 23 | 24 | // create a connection to the Solana blockchain 25 | const connection = new Connection( 26 | process.env.SOLANA_RPC_URL || clusterApiUrl("devnet"), 27 | "confirmed", 28 | ); 29 | 30 | /** 31 | * load a keypair from either the local file system or an env variable 32 | * then auto airdrop some sol if this keypair has a low balance 33 | */ 34 | console.log("Initializing keypair:", "payer"); 35 | const payer = await initializeKeypair(connection, { 36 | // keypairPath: DEFAULT_CLI_KEYPAIR_PATH, 37 | envVariableName: KEYPAIR_PAYER_ENV_NAME, 38 | }); 39 | 40 | console.log("Payer address:", payer.publicKey.toBase58(), "\n"); 41 | 42 | // get the current balance of the `payer` account on chain 43 | const currentBalance = await connection.getBalance(payer.publicKey); 44 | console.log("Current balance of 'payer' (in lamports):", currentBalance); 45 | console.log("Current balance of 'payer' (in SOL):", currentBalance / LAMPORTS_PER_SOL); 46 | 47 | ////////////////////////////////////////////////////////////////////////////// 48 | ////////////////////////////////////////////////////////////////////////////// 49 | 50 | // generate a new, random address to create on chain 51 | const keypair = Keypair.generate(); 52 | 53 | console.log("New keypair generated:", keypair.publicKey.toBase58()); 54 | 55 | /** 56 | * create a simple instruction (using web3.js) to create an account 57 | */ 58 | 59 | // on-chain space to allocated (in number of bytes) 60 | const space = 0; 61 | 62 | // request the cost (in lamports) to allocate `space` number of bytes on chain 63 | const lamports = await connection.getMinimumBalanceForRentExemption(space); 64 | 65 | console.log("Total lamports:", lamports); 66 | 67 | // create this simple instruction using web3.js helper function 68 | const createAccountIx = SystemProgram.createAccount({ 69 | // `fromPubkey` - this account will need to sign the transaction 70 | fromPubkey: payer.publicKey, 71 | // `newAccountPubkey` - the account address to create on chain 72 | newAccountPubkey: keypair.publicKey, 73 | // lamports to store in this account 74 | lamports, 75 | // total space to allocate 76 | space, 77 | // the owning program for this account 78 | programId: SystemProgram.programId, 79 | }); 80 | 81 | /** 82 | * build the transaction to send to the blockchain 83 | */ 84 | 85 | // get the latest recent blockhash 86 | let recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 87 | 88 | // create a message (v0) 89 | const message = new TransactionMessage({ 90 | payerKey: payer.publicKey, 91 | recentBlockhash, 92 | instructions: [createAccountIx], 93 | }).compileToV0Message(); 94 | 95 | // create a versioned transaction using the message 96 | const tx = new VersionedTransaction(message); 97 | 98 | // console.log("tx before signing:", tx); 99 | 100 | // sign the transaction with our needed Signers (e.g. `payer` and `keypair`) 101 | tx.sign([payer, keypair]); 102 | 103 | console.log("tx after signing:", tx); 104 | 105 | // tx.signatures.toString("base58") 106 | 107 | // console.log(tx.signatures); 108 | 109 | // actually send the transaction 110 | const txSig = await connection.sendTransaction(tx); 111 | 112 | console.log("Transaction completed."); 113 | console.log(getExplorerLink("transaction", txSig, "devnet")); 114 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { Connection, PublicKey } from "@solana/web3.js"; 3 | import { getExplorerLink } from "@solana-developers/helpers"; 4 | 5 | // define some default locations 6 | export const DEFAULT_KEY_DIR_NAME = ".local_keys"; 7 | export const DEFAULT_PUBLIC_KEY_FILE = "keys.json"; 8 | export const DEFAULT_DEMO_DATA_FILE = "demo.json"; 9 | 10 | export const DEFAULT_CLI_KEYPAIR_PATH = "~/.config/solana/id.json"; 11 | export const KEYPAIR_PAYER_ENV_NAME = "PAYER_KEYPAIR"; 12 | export const KEYPAIR_TESTER_ENV_NAME = "TESTER_KEYPAIR"; 13 | 14 | /** 15 | * Load locally stored PublicKey addresses 16 | */ 17 | export function loadPublicKeysFromFile( 18 | absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`, 19 | ) { 20 | try { 21 | if (!absPath) throw Error("No path provided"); 22 | if (!fs.existsSync(absPath)) throw Error("File does not exist."); 23 | 24 | // load the public keys from the file 25 | const data = JSON.parse(fs.readFileSync(absPath, { encoding: "utf-8" })) || {}; 26 | 27 | // convert all loaded keyed values into valid public keys 28 | for (const [key, value] of Object.entries(data)) { 29 | data[key] = new PublicKey(value as string) ?? ""; 30 | } 31 | 32 | return data; 33 | } catch (err) { 34 | // console.warn("Unable to load local file"); 35 | } 36 | // always return an object 37 | return {}; 38 | } 39 | 40 | /* 41 | Locally save a PublicKey addresses to the filesystem for later retrieval 42 | */ 43 | export function savePublicKeyToFile( 44 | name: string, 45 | publicKey: PublicKey, 46 | absPath: string = `${DEFAULT_KEY_DIR_NAME}/${DEFAULT_PUBLIC_KEY_FILE}`, 47 | ) { 48 | try { 49 | // if (!absPath) throw Error("No path provided"); 50 | // if (!fs.existsSync(absPath)) throw Error("File does not exist."); 51 | 52 | // fetch all the current values 53 | let data: any = loadPublicKeysFromFile(absPath); 54 | 55 | // convert all loaded keyed values from PublicKeys to strings 56 | for (const [key, value] of Object.entries(data)) { 57 | data[key as any] = (value as PublicKey).toBase58(); 58 | } 59 | data = { ...data, [name]: publicKey.toBase58() }; 60 | 61 | // actually save the data to the file 62 | fs.writeFileSync(absPath, JSON.stringify(data), { 63 | encoding: "utf-8", 64 | }); 65 | 66 | // reload the keys for sanity 67 | data = loadPublicKeysFromFile(absPath); 68 | 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 | /* 78 | Helper function to extract a transaction signature from a failed transaction's error message 79 | */ 80 | export async function extractSignatureFromFailedTransaction( 81 | connection: Connection, 82 | err: any, 83 | fetchLogs?: boolean, 84 | ) { 85 | if (err?.signature) return err.signature; 86 | 87 | // extract the failed transaction's signature 88 | const failedSig = new RegExp(/^((.*)?Error: )?(Transaction|Signature) ([A-Z0-9]{32,}) /gim).exec( 89 | err?.message?.toString(), 90 | )?.[4]; 91 | 92 | // ensure a signature was found 93 | if (failedSig) { 94 | // when desired, attempt to fetch the program logs from the cluster 95 | if (fetchLogs) 96 | await connection 97 | .getTransaction(failedSig, { 98 | maxSupportedTransactionVersion: 0, 99 | }) 100 | .then(tx => { 101 | console.log(`\n==== Transaction logs for ${failedSig} ====`); 102 | console.log(getExplorerLink("transaction", failedSig, "devnet")); 103 | console.log(tx?.meta?.logMessages ?? "No log messages provided by RPC"); 104 | console.log(`==== END LOGS ====\n`); 105 | }); 106 | else { 107 | console.log("\n========================================"); 108 | console.log({ txSignature: failedSig }); 109 | console.log("========================================\n"); 110 | } 111 | } 112 | 113 | // always return the failed signature value 114 | return failedSig; 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Workshop: Solana Basics 2 | 3 | ## Quick links to learn more 4 | 5 | - https://solana.com/developers 6 | - https://solana.com/developers/guides 7 | - Slides from the workshop: 8 | [Google Slides](https://docs.google.com/presentation/d/1BpObg9lOMllUxdcHJQ5EpJGVGGiNmwcS1QUysbs91y0/edit?usp=sharing) 9 | 10 | ## Tech stack used 11 | 12 | - uses TypeScript and NodeJS 13 | - `npm` (as the package manager) 14 | 15 | ## Setup locally 16 | 17 | 1. Clone this repo to your local system 18 | 2. Install the packages via `npm install` 19 | 20 | ## Recommended flow to explore this repo 21 | 22 | After getting setup locally, we recommend exploring the code of the following files (in order): 23 | 24 | - [`0.basics.ts`](./scripts/0.basics.ts) 25 | - [`1.simpleTransaction.ts`](./scripts/1.simpleTransaction.ts) 26 | - [`2.complexTransaction.ts`](./scripts/2.complexTransaction.ts) 27 | 28 | ### Running the included Scripts 29 | 30 | Once setup locally, you will be able to run the scripts included within this repo: 31 | 32 | ```shell 33 | npx esrun ./scripts/