From 0823b6a09d1a1bf1aa4f6e46940dd22ce0cfc2c1 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Thu, 21 Nov 2024 19:06:15 +0100 Subject: [PATCH] Add web3jsV2 examples (#610) * Add web3jsV2 examples * Code review * Update add-priority-fees.md * Fix formatting * Update add-priority-fees.md --------- Co-authored-by: nickfrosty <75431177+nickfrosty@users.noreply.github.com> --- .../development/load-keypair-from-file.md | 87 ++++++++++ content/cookbook/transactions/add-memo.md | 127 ++++++++++++++- .../transactions/add-priority-fees.md | 126 +++++++++++++++ .../cookbook/transactions/calculate-cost.md | 151 +++++++++++++++++- content/cookbook/transactions/send-sol.md | 125 +++++++++++++++ 5 files changed, 612 insertions(+), 4 deletions(-) create mode 100644 content/cookbook/development/load-keypair-from-file.md diff --git a/content/cookbook/development/load-keypair-from-file.md b/content/cookbook/development/load-keypair-from-file.md new file mode 100644 index 000000000..b7ada9236 --- /dev/null +++ b/content/cookbook/development/load-keypair-from-file.md @@ -0,0 +1,87 @@ +--- +title: Load a local json file keypair +sidebarSortOrder: 6 +description: "Learn how to load a keypair from file." +--- + +When running your local project you probably want to use a file json keypair. +This can be very useful for all the cookbook examples as well. You can grind +yourself a keypair using `solana-keygen grind --starts-with a23:1` and then load +and use this one for your projects using the `loadKeypairFromFile` function. + +```typescript filename="load-keypair-from-file.ts" +import { + airdropFactory, + createKeyPairFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + devnet, + generateKeyPair, + getAddressFromPublicKey, + lamports, +} from "@solana/web3.js"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +// The new library takes a brand-new approach to Solana key pairs and addresses, +// which will feel quite different from the classes PublicKey and Keypair from version 1.x. +// All key operations now use the native Ed25519 implementation in JavaScript’s +// Web Crypto API. +async function createKeypair() { + const newKeypair: CryptoKeyPair = await generateKeyPair(); + const publicAddress = await getAddressFromPublicKey(newKeypair.publicKey); + + console.log(`Public key: ${publicAddress}`); +} + +export async function loadDefaultKeypair(): Promise { + return await loadKeypairFromFile("~/.config/solana/id.json"); +} + +export async function loadDefaultKeypairWithAirdrop( + cluster: string, +): Promise { + const keypair = await loadDefaultKeypair(); + const rpc = createSolanaRpc(devnet(`https://api.${cluster}.solana.com`)); + const rpcSubscriptions = createSolanaRpcSubscriptions( + devnet(`wss://api.${cluster}.solana.com`), + ); + try { + const result = await rpc + .getBalance(await getAddressFromPublicKey(keypair.publicKey)) + .send(); + + console.log(`Balance: ${result.value} lamports`); + if (result.value < lamports(500_000n)) { + console.log(`Balance low requesting airdrop`); + const airdrop = airdropFactory({ rpc, rpcSubscriptions }); + await airdrop({ + commitment: "confirmed", + lamports: lamports(1000_000n), + recipientAddress: await getAddressFromPublicKey(keypair.publicKey), + }); + } + } catch (err) { + console.error("Error fetching balance:", err); + } + return keypair; +} + +export async function loadKeypairFromFile( + filePath: string, +): Promise { + // This is here so you can also load the default keypair from the file system. + const resolvedPath = path.resolve( + filePath.startsWith("~") ? filePath.replace("~", os.homedir()) : filePath, + ); + const loadedKeyBytes = Uint8Array.from( + JSON.parse(fs.readFileSync(resolvedPath, "utf8")), + ); + // Here you can also set the second parameter to true in case you need to extract your private key. + const keypair = await createKeyPairFromBytes(loadedKeyBytes); + return keypair; +} + +createKeypair(); +``` diff --git a/content/cookbook/transactions/add-memo.md b/content/cookbook/transactions/add-memo.md index 93deb235e..490566307 100644 --- a/content/cookbook/transactions/add-memo.md +++ b/content/cookbook/transactions/add-memo.md @@ -6,9 +6,126 @@ description: how to add a memo to your transactions on Solana." --- -Any transaction can add a message making use of the memo program. Currently the -programID from the Memo Program has to be added manually -`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`. +Any transaction can add a message making use of the memo program. In web3.js@1 +the programID from the Memo Program has to be added manually +`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr` in V2 you can use +`getAddMemoInstruction`. + + + + + +```typescript filename="add-memo.ts" {61-72} +import { + airdropFactory, + appendTransactionMessageInstructions, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + devnet, + generateKeyPairSigner, + getComputeUnitEstimateForTransactionMessageFactory, + getSignatureFromTransaction, + lamports, + pipe, + prependTransactionMessageInstructions, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, + type Transaction, +} from "@solana/web3.js"; +import { + getSetComputeUnitLimitInstruction, + getSetComputeUnitPriceInstruction, +} from "@solana-program/compute-budget"; +import { getAddMemoInstruction } from "@solana-program/memo"; + +async function writeMemo(message: string) { + // Create an RPC. + const CLUSTER = "devnet"; + const rpc = createSolanaRpc(devnet(`https://api.${CLUSTER}.solana.com`)); + const rpcSubscriptions = createSolanaRpcSubscriptions( + devnet(`wss://api.${CLUSTER}.solana.com`), + ); + + // Create an airdrop function. + const airdrop = airdropFactory({ rpc, rpcSubscriptions }); + + // Create a utility that estimates a transaction message's compute consumption. + const getComputeUnitEstimate = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + // Create a transaction sending function. + const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ + rpc, + rpcSubscriptions, + }); + + // Create and fund an account. + const keypairSigner = await generateKeyPairSigner(); + console.log("Created an account with address", keypairSigner.address); + console.log("Requesting airdrop"); + await airdrop({ + commitment: "confirmed", + lamports: lamports(1000_000n), + recipientAddress: keypairSigner.address, + }); + console.log("Airdrop confirmed"); + + // Create a memo transaction. + console.log("Creating a memo transaction"); + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + const transactionMessage = pipe( + createTransactionMessage({ version: "legacy" }), + m => setTransactionMessageFeePayerSigner(keypairSigner, m), + m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), + m => + appendTransactionMessageInstructions( + [ + getSetComputeUnitPriceInstruction({ microLamports: 5000n }), + getAddMemoInstruction({ memo: message }), + ], + m, + ), + ); + + // Figure out how many compute units to budget for this transaction + // so that you can right-size the compute budget to maximize the + // chance that it will be selected for inclusion into a block. + console.log("Estimating the compute consumption of the transaction"); + const estimatedComputeUnits = + await getComputeUnitEstimate(transactionMessage); + console.log( + `Transaction is estimated to consume ${estimatedComputeUnits} compute units`, + ); + const budgetedTransactionMessage = prependTransactionMessageInstructions( + [getSetComputeUnitLimitInstruction({ units: estimatedComputeUnits })], + transactionMessage, + ); + + // Sign and send the transaction. + console.log("Signing and sending the transaction"); + const signedTx = await signTransactionMessageWithSigners( + budgetedTransactionMessage, + ); + const signature = getSignatureFromTransaction(signedTx); + console.log( + "Sending transaction https://explorer.solana.com/tx/" + + signature + + "/?cluster=" + + CLUSTER, + ); + await sendAndConfirmTransaction(signedTx, { commitment: "confirmed" }); + console.log("Transaction confirmed"); +} + +writeMemo("Hello, Solana!"); +``` + + + + ```typescript filename="add-memo.ts" {38-46} import { @@ -63,3 +180,7 @@ import { ]); })(); ``` + + + + diff --git a/content/cookbook/transactions/add-priority-fees.md b/content/cookbook/transactions/add-priority-fees.md index a10b49b42..ea02cb42f 100644 --- a/content/cookbook/transactions/add-priority-fees.md +++ b/content/cookbook/transactions/add-priority-fees.md @@ -29,6 +29,128 @@ compute budget. The value provided will replace the default value. Transactions should request the minimum amount of CU required for execution to maximize throughput, or minimize fees. + + + + +```typescript filename="add-priority-fees.ts" {61-72} {37-38} {77-87} +import { + airdropFactory, + appendTransactionMessageInstructions, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + devnet, + generateKeyPairSigner, + getComputeUnitEstimateForTransactionMessageFactory, + getSignatureFromTransaction, + lamports, + pipe, + prependTransactionMessageInstructions, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, +} from "@solana/web3.js"; +import { + getSetComputeUnitLimitInstruction, + getSetComputeUnitPriceInstruction, +} from "@solana-program/compute-budget"; +import { getAddMemoInstruction } from "@solana-program/memo"; + +async function writeMemoWithPriorityFees(message: string) { + // Create an RPC. + const CLUSTER = "devnet"; + const rpc = createSolanaRpc(devnet(`https://api.${CLUSTER}.solana.com`)); + const rpcSubscriptions = createSolanaRpcSubscriptions( + devnet(`wss://api.${CLUSTER}.solana.com`), + ); + + // Create an airdrop function. + const airdrop = airdropFactory({ rpc, rpcSubscriptions }); + + // Create a utility that estimates a transaction message's compute consumption. + const getComputeUnitEstimate = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + // Create a transaction sending function. + const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ + rpc, + rpcSubscriptions, + }); + + // Create and fund an account. + const keypairSigner = await generateKeyPairSigner(); + console.log("Created an account with address", keypairSigner.address); + console.log("Requesting airdrop"); + await airdrop({ + commitment: "confirmed", + lamports: lamports(1000_000n), + recipientAddress: keypairSigner.address, + }); + console.log("Airdrop confirmed"); + + // Create a memo transaction. + console.log("Creating a memo transaction"); + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + const transactionMessage = pipe( + createTransactionMessage({ version: "legacy" }), + m => setTransactionMessageFeePayerSigner(keypairSigner, m), + m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), + m => + appendTransactionMessageInstructions( + [ + getSetComputeUnitPriceInstruction({ microLamports: 5000n }), + getAddMemoInstruction({ memo: message }), + ], + m, + ), + ); + + // Figure out how many compute units to budget for this transaction + // so that you can right-size the compute budget to maximize the + // chance that it will be selected for inclusion into a block. + console.log("Estimating the compute consumption of the transaction"); + var estimatedComputeUnits = await getComputeUnitEstimate(transactionMessage); + // While these estimates are quite accurate they are not perfect. So you may want to add a + // buffer if you expect that the transaction may consume more compute units than estimated. + // Its not possible to exactly know what the transaction will consume when + // you send it in the future. The state may change. You can add a buffer to the estimate to account for this. + // estimatedComputeUnits += 1000; + // estimatedComputeUnits *= 1.1; + // You can read more about the issue here: https://github.com/solana-labs/solana-web3.js/tree/master/packages/library#getcomputeunitestimatefortransactionmessagefactoryrpc + + console.log( + `Transaction is estimated to consume ${estimatedComputeUnits} compute units`, + ); + const budgetedTransactionMessage = prependTransactionMessageInstructions( + [getSetComputeUnitLimitInstruction({ units: estimatedComputeUnits })], + transactionMessage, + ); + + // Sign and send the transaction. + console.log("Signing and sending the transaction"); + const signedTx = await signTransactionMessageWithSigners( + budgetedTransactionMessage, + ); + const signature = getSignatureFromTransaction(signedTx); + console.log( + "Sending transaction https://explorer.solana.com/tx/" + + signature + + "/?cluster=" + + CLUSTER, + ); + await sendAndConfirmTransaction(signedTx, { commitment: "confirmed" }); + console.log("Transaction confirmed"); +} + +writeMemoWithPriorityFees("Hello, priority fees!"); +``` + + + + + ```typescript filename="add-priority-fees.ts" {25-28, 30-33} import { BN } from "@coral-xyz/anchor"; import { @@ -85,3 +207,7 @@ import { console.log(result); })(); ``` + + + + diff --git a/content/cookbook/transactions/calculate-cost.md b/content/cookbook/transactions/calculate-cost.md index 015a5824e..9bf42df2f 100644 --- a/content/cookbook/transactions/calculate-cost.md +++ b/content/cookbook/transactions/calculate-cost.md @@ -11,7 +11,152 @@ transaction cost. As long as you are not creating an account, this will be the base transaction cost. To find out more about costs to create an account, check out [calculating rent costs](/content/cookbook/accounts/calculate-rent.md). -```typescript filename="calculate-cost.ts" + + + + +```typescript filename="calculate-cost.ts" {101-118} +import { + airdropFactory, + appendTransactionMessageInstructions, + compileTransactionMessage, + createSignerFromKeyPair, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + devnet, + generateKeyPairSigner, + getBase64Decoder, + getCompiledTransactionMessageEncoder, + getComputeUnitEstimateForTransactionMessageFactory, + getSignatureFromTransaction, + lamports, + pipe, + prependTransactionMessageInstructions, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, + type TransactionMessageBytesBase64, +} from "@solana/web3.js"; +import { + getSetComputeUnitLimitInstruction, + getSetComputeUnitPriceInstruction, +} from "@solana-program/compute-budget"; +import { getAddMemoInstruction } from "@solana-program/memo"; +import { loadDefaultKeypairWithAirdrop } from "./CreateKeypair"; + +async function calculateCost(message: string) { + // Create an RPC. + const CLUSTER = "devnet"; + const rpc = createSolanaRpc(devnet(`https://api.${CLUSTER}.solana.com`)); + const rpcSubscriptions = createSolanaRpcSubscriptions( + devnet(`wss://api.${CLUSTER}.solana.com`), + ); + + // Create a utility that estimates a transaction message's compute consumption. + const getComputeUnitEstimate = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + // Create a transaction sending function. + const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ + rpc, + rpcSubscriptions, + }); + + // Create an airdrop function. + const airdrop = airdropFactory({ rpc, rpcSubscriptions }); + + // Create and fund an account. + const signer = await generateKeyPairSigner(); + console.log("Created an account with address", signer.address); + console.log("Requesting airdrop"); + await airdrop({ + commitment: "confirmed", + lamports: lamports(1000_000n), + recipientAddress: signer.address, + }); + console.log("Airdrop confirmed"); + + // Create a memo transaction. + console.log("Creating a memo transaction"); + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + const transactionMessage = pipe( + createTransactionMessage({ version: "legacy" }), + m => setTransactionMessageFeePayerSigner(signer, m), + m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), + m => + appendTransactionMessageInstructions( + [ + getSetComputeUnitPriceInstruction({ microLamports: 5000n }), + getAddMemoInstruction({ memo: message }), + ], + m, + ), + ); + + // Figure out how many compute units to budget for this transaction + // so that you can right-size the compute budget to maximize the + // chance that it will be selected for inclusion into a block. + console.log("Estimating the compute consumption of the transaction"); + const estimatedComputeUnits = + await getComputeUnitEstimate(transactionMessage); + console.log( + `Transaction is estimated to consume ${estimatedComputeUnits} compute units`, + ); + + const budgetedTransactionMessage = prependTransactionMessageInstructions( + [getSetComputeUnitLimitInstruction({ units: estimatedComputeUnits })], + transactionMessage, + ); + + const base64EncodedMessage = pipe( + // Start with the message you want the fee for. + budgetedTransactionMessage, + + // Compile it. + compileTransactionMessage, + + // Convert the compiled message into a byte array. + getCompiledTransactionMessageEncoder().encode, + + // Encode that byte array as a base64 string. + getBase64Decoder().decode, + ) as TransactionMessageBytesBase64; + + const transactionCost = await rpc + .getFeeForMessage(base64EncodedMessage) + .send(); + + console.log( + "Transaction is estimated to cost " + transactionCost.value + " lamports", + ); + + // Sign and send the transaction. + console.log("Signing and sending the transaction"); + const signedTx = await signTransactionMessageWithSigners( + budgetedTransactionMessage, + ); + const signature = getSignatureFromTransaction(signedTx); + console.log( + "Sending transaction https://explorer.solana.com/tx/" + + signature + + "/?cluster=" + + CLUSTER, + ); + await sendAndConfirmTransaction(signedTx, { commitment: "confirmed" }); + console.log("Transaction confirmed"); + // Transaction is estimated to consume 6236 compute units + // Transaction is estimated to cost 5032 lamports +} + +calculateCost("Hello, Fees!"); +``` + + + + +```typescript filename="calculate-cost.ts {108-111}" import { clusterApiUrl, Connection, @@ -65,3 +210,7 @@ import bs58 from "bs58"; // Estimated SOL transfer cost: 5000 lamports })(); ``` + + + + diff --git a/content/cookbook/transactions/send-sol.md b/content/cookbook/transactions/send-sol.md index 5cfe36ee0..67003ef34 100644 --- a/content/cookbook/transactions/send-sol.md +++ b/content/cookbook/transactions/send-sol.md @@ -8,6 +8,127 @@ description: To send SOL, you will need to interact with the [SystemProgram][1]. + + + + +```typescript filename="send-sol.ts" {70-74} +import { + address, + airdropFactory, + appendTransactionMessageInstructions, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + devnet, + generateKeyPairSigner, + getComputeUnitEstimateForTransactionMessageFactory, + getSignatureFromTransaction, + lamports, + pipe, + prependTransactionMessageInstructions, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, +} from "@solana/web3.js"; +import { + getSetComputeUnitLimitInstruction, + getSetComputeUnitPriceInstruction, +} from "@solana-program/compute-budget"; +import { getAddMemoInstruction } from "@solana-program/memo"; +import { getTransferSolInstruction } from "@solana-program/system"; + +async function transferSol() { + // Create an RPC. Use localnet for solana-test-validator. This will get you easier airdrops. + const CLUSTER = "devnet"; + const rpc = createSolanaRpc(devnet(`https://api.${CLUSTER}.solana.com`)); + const rpcSubscriptions = createSolanaRpcSubscriptions( + devnet(`wss://api.${CLUSTER}.solana.com`), + ); + + // Create an airdrop function. + const airdrop = airdropFactory({ rpc, rpcSubscriptions }); + + // Create a utility that estimates a transaction message's compute consumption. + const getComputeUnitEstimate = + getComputeUnitEstimateForTransactionMessageFactory({ rpc }); + + // Create a transaction sending function. + const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ + rpc, + rpcSubscriptions, + }); + + // Create and fund an account. + const keypairSigner = await generateKeyPairSigner(); + console.log("Created an account with address", keypairSigner.address); + console.log("Requesting airdrop"); + await airdrop({ + commitment: "confirmed", + lamports: lamports(1000_000n), + recipientAddress: keypairSigner.address, + }); + console.log("Airdrop confirmed"); + + // Create a memo transaction. + console.log("Creating a memo transaction"); + const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); + const transactionMessage = pipe( + createTransactionMessage({ version: "legacy" }), + m => setTransactionMessageFeePayerSigner(keypairSigner, m), + m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), + m => + appendTransactionMessageInstructions( + [ + getSetComputeUnitPriceInstruction({ microLamports: 5000n }), + getTransferSolInstruction({ + source: keypairSigner, + destination: address("web3Qm5PuFapMJqe6PWRWfRBarkeqE2ZC8Eew3zwHH2"), + amount: lamports(1_000n), + }), + ], + m, + ), + ); + + // Figure out how many compute units to budget for this transaction + // so that you can right-size the compute budget to maximize the + // chance that it will be selected for inclusion into a block. + console.log("Estimating the compute consumption of the transaction"); + const estimatedComputeUnits = + await getComputeUnitEstimate(transactionMessage); + console.log( + `Transaction is estimated to consume ${estimatedComputeUnits} compute units`, + ); + const budgetedTransactionMessage = prependTransactionMessageInstructions( + [getSetComputeUnitLimitInstruction({ units: estimatedComputeUnits })], + transactionMessage, + ); + + // Sign and send the transaction. + console.log("Signing and sending the transaction"); + const signedTx = await signTransactionMessageWithSigners( + budgetedTransactionMessage, + ); + const signature = getSignatureFromTransaction(signedTx); + console.log( + "Sending transaction https://explorer.solana.com/tx/" + + signature + + "/?cluster=" + + CLUSTER, + ); + await sendAndConfirmTransaction(signedTx, { commitment: "confirmed" }); + console.log("Transaction confirmed"); +} + +transferSol(); +``` + + + + + ```typescript filename="send-sol.ts" {28-38} import { Connection, @@ -50,4 +171,8 @@ import { })(); ``` + + + + [1]: https://docs.solanalabs.com/runtime/programs#system-program