diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..0d582aa --- /dev/null +++ b/README-en.md @@ -0,0 +1,56 @@ +# Solana Web3.js Tutorial + +> This tutorial is written by members of the Buff community and is updated weekly with 1-3 sections. + +The Solana Web3.js library is a JavaScript-based library for interacting with the Solana blockchain. + +This tutorial aims to provide simple examples to help you quickly get started with Solana blockchain development. + +--- + +Before reading, you need to run the following command to install @solana/web3.js. The version used in this tutorial is 1.95.4. + +``` +$ npm install @solana/web3.js@1.95.4 +``` + +After that, you can run the code for each section using `npx esrun xxx/index.ts`. + +## Table of Contents + +0. Basic Terminology + +### Basics + +1. [Create Wallet & Import Wallet](./en/01-wallet/) +2. [Get SOL Balance of an Account](./en/02-balance/) +3. [Send Your First Transfer Transaction](./en/03-transfer/) +4. [Read Data from the Chain (1): `get` read](./en/04-get/) +5. [Read Data from the Chain (2): `on` subscription](./en/05-on/) +6. [Send Data to the Chain: `send` transaction](./en/06-send/) + +### Advanced + +7. [Add Priority Fee](./en/07-cu/) +8. [`v0` Transaction](./en/08-v0/) +9. [Parse Buffer](./en/09-buffer/) +10. + +### Practical + +1. [Listen to Wallet](./en/example-01-subWallet/) +2. [Listen to raydium v4 new liquidity pool creation](./en/example-02-subNewPool/) +3. [Listen to real-time price of raydium clmm token](./en/example-03-subPrice/) +4. + +## References + +- https://solana-labs.github.io/solana-web3.js/v1.x +- https://solana.com/zh/developers/cookbook +- https://solana.com/docs + +## Donation + +If you want to support the development of the Buff community, you can donate Solana chain assets to the `buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ` address. + +Community funds will be used to reward community contributors, including but not limited to `PR`. \ No newline at end of file diff --git a/README.md b/README.md index 0469783..f893db8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Solana Web3.js 库是一个用于与 Solana 区块链进行交互的 JavaScript 本教程旨在提供一些简单的例子,帮助你快速上手 Solana 区块链开发。 +> English version (英文版): [README-en.md](./README-en.md) + --- 在阅读之前,需要运行如下来安装@solana/web3.js,本教程使用的版本为1.95.4。 diff --git a/en/01-wallet/README.md b/en/01-wallet/README.md new file mode 100644 index 0000000..2035bcc --- /dev/null +++ b/en/01-wallet/README.md @@ -0,0 +1,68 @@ +# Create Wallet & Import Wallet + +> The private key is the only credential for the account; please keep it safe. + +This section will introduce how to create your own wallet using the web3.js library. + +## Create Wallet + +A Solana wallet refers to a pair of `private key` and `public key`, which are the credentials used to access and manage accounts on Solana. The key pair is generated through random numbers to ensure that each key pair is unique. + +- Private Key: A confidential key used to prove account ownership. The private key can be used to generate digital signatures and authorize transactions. If the private key is leaked, others can use it to control your account. +- Public Key: The public part paired with the private key. The public key is your account address, and others can send assets to you or query your account balance through the public key, but they cannot use it to authorize operations. + +```ts +import { Keypair } from "@solana/web3.js"; +import fs from "fs"; +import { Buffer } from 'buffer'; + +// Create wallet +const wallet = Keypair.generate(); + +// Get public key and private key +const publicKey = wallet.publicKey.toBase58(); +const secretKey = wallet.secretKey; // A Uint8Array + +// Print +console.log("Wallet Public Key:", publicKey); +console.log("Wallet Private Key:", secretKey); +console.log("Wallet Private Key (base64):", Buffer.from(secretKey).toString("base64")); + +// Save Uint8Array private key +fs.writeFileSync("wallet.json", JSON.stringify(Array.from(secretKey))); +``` + +Run with `npx esrun 01-wallet/index.ts`, the output is as follows: + +```bash +Wallet Public Key: EkfAVHeFtDUmGQJH5e67i784wKKNA7jyStKywQWysY73 +Wallet Private Key: Uint8Array(64) [ + 180, 206, 18, 236, 242, 179, 168, 142, 181, 66, + 158, 123, 232, 162, 205, 195, 192, 56, 117, 152, + 238, 67, 141, 162, 250, 60, 104, 153, 79, 96, + 49, 234, 204, 87, 14, 120, 218, 77, 112, 188, + 235, 139, 1, 134, 201, 208, 112, 25, 2, 151, + 227, 188, 25, 69, 178, 196, 146, 227, 179, 14, + 118, 115, 233, 234 +] +Wallet Private Key (base64): tM4S7PKzqI61Qp576KLNw8A4dZjuQ42i+jxomU9gMerMVw542k1wvOuLAYbJ0HAZApfjvBlFssSS47MOdnPp6g== +``` + +The private key is saved in the `wallet.json` file in the root directory of this project. The public key is 32 bytes long, usually encoded in Base58; the private key is 64 bytes long, usually encoded in Base64. + +## Import Wallet + +Import the private key from the newly saved `wallet.json` file to restore the wallet. + +```ts +const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const wallet = Keypair.fromSecretKey(secretKey); + +console.log("Wallet Public Key:", wallet.publicKey.toString()); +console.log("Wallet Private Key:", wallet.secretKey); +console.log("Wallet Private Key (base64):", Buffer.from(secretKey).toString("base64")); +``` + +The output after running should be consistent with the previous output. + + diff --git a/en/01-wallet/index.ts b/en/01-wallet/index.ts new file mode 100644 index 0000000..105d74e --- /dev/null +++ b/en/01-wallet/index.ts @@ -0,0 +1,26 @@ +import { Keypair } from "@solana/web3.js"; +import fs from "fs"; +import { Buffer } from 'buffer'; + +// Create a wallet +const wallet = Keypair.generate(); + +// Get the public key and secret key +const publicKey = wallet.publicKey.toBase58(); +const secretKey = wallet.secretKey; // A Uint8Array + +// Print keys +console.log("Wallet Public Key:", publicKey); +console.log("Wallet Secret Key:", secretKey); +console.log("Wallet Secret Key (base64):", Buffer.from(secretKey).toString("base64")); + +// Save the Uint8Array secret key +fs.writeFileSync("wallet.json", JSON.stringify(Array.from(secretKey))); + +// Import the wallet +// const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +// const wallet = Keypair.fromSecretKey(secretKey); + +// console.log("Wallet Public Key:", wallet.publicKey.toString()); +// console.log("Wallet Secret Key:", wallet.secretKey); +// console.log("Wallet Secret Key (base64):", Buffer.from(secretKey).toString("base64")); diff --git a/en/02-balance/README.md b/en/02-balance/README.md new file mode 100644 index 0000000..8e6166a --- /dev/null +++ b/en/02-balance/README.md @@ -0,0 +1,48 @@ +# Get SOL Balance of an Account + +> The RPC port is the medium for interacting with the Solana blockchain. + +This section will introduce how to create an RPC connection using the `Connection` class and use the `getBalance` method to retrieve the SOL balance of an account. + +## Creating an RPC Connection + +The `Connection` class is the core class for interacting with the Solana blockchain, providing various methods to communicate with the blockchain. An instance of `Connection` can be created by providing the RPC port and confirmation level, as shown below: + +```ts +import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +``` + +`https://api.mainnet-beta.solana.com` is the official RPC port provided by Solana, and `confirmed` is the default confirmation level. + +> Note: +> `processed` is a lower confirmation level, meaning the queried data is verified but not fully confirmed. `confirmed` indicates that the node has written the transaction to the blockchain, but it may not be finally confirmed. If a higher confirmation level is needed, `finalized` can be used. + +## Querying Account Balance + +The `getBalance` method is used to query the SOL balance of a specified account, returning the balance in lamports, which needs to be divided by `LAMPORTS_PER_SOL` to convert to SOL units. + +> Note: +> `LAMPORTS_PER_SOL` is the number of lamports in 1 SOL, equal to 10^9. + +Here, we will query the SOL balance of Jito1, whose public key is `CXPeim1wQMkcTvEHx9QdhgKREYYJD8bnaCCqPRwJ1to1`. + +```ts +async function main() { + + // Query Jito1's SOL balance + const publicKey = new PublicKey('CXPeim1wQMkcTvEHx9QdhgKREYYJD8bnaCCqPRwJ1to1'); + const balance = await connection.getBalance(publicKey); + console.log(`Jito1 balance: ${balance / LAMPORTS_PER_SOL} SOL`); // Convert to SOL units +} + +main(); +``` + +By running the above code with `npx esrun 02-balance/index.ts`, you can see that Jito1's current SOL balance is 9.999906999 SOL. + +``` +Jito1 balance: 9.999906999 SOL +``` \ No newline at end of file diff --git a/en/02-balance/index.ts b/en/02-balance/index.ts new file mode 100644 index 0000000..bad1143 --- /dev/null +++ b/en/02-balance/index.ts @@ -0,0 +1,15 @@ +import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +async function main() { + + // Query the SOL balance of Jito1 + const publicKey = new PublicKey('CXPeim1wQMkcTvEHx9QdhgKREYYJD8bnaCCqPRwJ1to1'); + const balance = await connection.getBalance(publicKey); + console.log(`Jito1 balance: ${balance / LAMPORTS_PER_SOL} SOL`); // Convert to SOL unit +} + +main(); \ No newline at end of file diff --git a/en/03-transfer/README.md b/en/03-transfer/README.md new file mode 100644 index 0000000..ef8d0ef --- /dev/null +++ b/en/03-transfer/README.md @@ -0,0 +1,87 @@ +# Sending Your First Transfer Transaction + +> Sending a transaction is the only way to change the on-chain state. + +This section will introduce how to create and send your first transfer transaction. + +## Creating an RPC Connection and Importing a Wallet + +First, as we learned earlier, we need to create an RPC connection and import our wallet private key. + +```ts +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + sendAndConfirmTransaction +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); + +// Import wallet locally +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); +``` + +## Creating, Simulating, and Sending a Transfer Transaction + +Next, we will create a transaction and add a transfer instruction to `buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ`. After that, we can first simulate whether the transaction will succeed. If the simulation is successful, we will actually send the transaction. + +> Note: +> `buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ` is the public fund account of the Buff community and can be replaced with any other account. + +```ts +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + console.log(`Transaction sent, https://solscan.io/tx/${signature}`); +} + +main(); +``` + +After running the script with `npx esrun 03-transfer/index.ts`, the output should be as follows: + +```bash +Simulation result: { + context: { apiVersion: '2.0.3', slot: 300547622 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 150 + } +} +Transaction sent: https://solscan.io/tx/3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC +``` + +Through this transaction, we successfully transferred 1000 lamports to `buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ`, and you can view this transaction on the blockchain explorer [here](https://solscan.io/tx/3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC). \ No newline at end of file diff --git a/en/03-transfer/index.ts b/en/03-transfer/index.ts new file mode 100644 index 0000000..fee5310 --- /dev/null +++ b/en/03-transfer/index.ts @@ -0,0 +1,45 @@ +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + sendAndConfirmTransaction +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +// Local import of wallet +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); + +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); \ No newline at end of file diff --git a/en/04-get/README.md b/en/04-get/README.md new file mode 100644 index 0000000..445c72c --- /dev/null +++ b/en/04-get/README.md @@ -0,0 +1,268 @@ +# Reading On-Chain Data (Part 1): `get` Read + +This section will introduce you to some commonly used methods for reading on-chain data and their use cases. + +These methods belong to the `Connection` class and are for single on-chain data reads, with method names starting with `get`. + +Before using these methods, you need to create an RPC connection. + +```ts +import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +``` + +## 1. getSlot + +Get the current slot. + +```ts +const slot = await connection.getSlot(); +console.log(`Current slot: ${slot}\n`); +``` + +``` +Current slot: 300579039 +``` + +## 2. getBalance + +Get the SOL balance of a specified account. + +```ts +// Query the SOL balance of Jito1 +const balance = await connection.getBalance(new PublicKey("CXPeim1wQMkcTvEHx9QdhgKREYYJD8bnaCCqPRwJ1to1")); +console.log(`J1to1 balance: ${balance / LAMPORTS_PER_SOL} SOL\n`); +``` + +``` +J1to1 balance: 12.172897148 SOL +``` + +## 3. getTokenAccountBalance + +Query the balance of a specified token account. + +> Solana assigns a token account to each token holder, and each account needs to pay 0.002 SOL in rent. When you no longer need this token account, you can choose to close it and reclaim the rent. + +```ts +// Get the USDC balance of the account web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2 +// https://solscan.io/account/HGtAdvmncQSk59mAxdh2M7GTUq1aB9WTwh7w7LwvbTBT +const tokenAccountBalance = await connection.getTokenAccountBalance(new PublicKey("HGtAdvmncQSk59mAxdh2M7GTUq1aB9WTwh7w7LwvbTBT")); +console.log(`Token account balance: ${JSON.stringify(tokenAccountBalance)}\n`); +``` + +``` +Token account balance: {"context":{"apiVersion":"2.0.3","slot":300580444},"value":{"amount":"2000000","decimals":6,"uiAmount":2,"uiAmountString":"2"}} +``` + +## 4. getFirstAvailableBlock + +Get the earliest block number accessible by the current RPC node. + +> Due to the large amount of data on Solana, general RPC nodes cannot store all block data and can only keep a small snapshot locally. Therefore, if you want to analyze historical transactions in blocks, purchasing a large RPC service provider's node is a good choice. + +```ts +const firstAvailableBlock = await connection.getFirstAvailableBlock(); +console.log(`First available block: ${firstAvailableBlock}\n`); +``` + +``` +First available block: 300439300 +``` + +## 5. getLatestBlockhash + +Get the latest block hash. + +> This is often called when sending transactions. + +```ts +const latestBlockhash = await connection.getLatestBlockhash(); +console.log(`Latest block hash: ${latestBlockhash.blockhash}\n`); +``` + +``` +Latest block hash: Hik7iYgKiALmPXp8HTAqok3kDQuSYWCaPLNa7rLNeu6v +``` + +## 6. getParsedAccountInfo + +Get detailed information about a parsed account. + +> This is very useful when obtaining information about on-chain liquidity pools. + +`getMultipleParsedAccounts` can query multiple account information at once. + +```ts +// Get the account information of the raydium clmm WSOL/USDC liquidity pool +const accountInfo = await connection.getAccountInfo(new PublicKey('8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj'), "confirmed"); +console.log("Account information:", accountInfo) +``` + +``` +{ + data: , + executable: false, + lamports: 865597210, + owner: PublicKey [PublicKey(CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK)] { + _bn: + }, + rentEpoch: 18446744073709552000, + space: 1544 +} +``` + +## 7. getParsedTransaction + +Get detailed information about a parsed transaction. + +`getParsedTransactions` can query multiple transactions at once. + +> This is very useful in scenarios where transaction parsing is needed. + +```ts +// Parse a SOL transfer transaction +const parsedTransaction = await connection.getParsedTransaction('3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC', { + commitment: "confirmed", + maxSupportedTransactionVersion: 0 +}); +console.log(`Parsed transaction: ${JSON.stringify(parsedTransaction)}\n`); +``` + +``` +Parsed transaction: {"blockTime":1731232782,"meta":{"computeUnitsConsumed":150,"err":null,"fee":5000,"innerInstructions":[],"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"postBalances":[7826454,1993200,1],"postTokenBalances":[],"preBalances":[7832454,1992200,1],"preTokenBalances":[],"rewards":[],"status":{"Ok":null}},"slot":300547625,"transaction":{"message":{"accountKeys":[{"pubkey":"web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2","signer":true,"source":"transaction","writable":true},{"pubkey":"buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ","signer":false,"source":"transaction","writable":true},{"pubkey":"11111111111111111111111111111111","signer":false,"source":"transaction","writable":false}],"instructions":[{"parsed":{"info":{"destination":"buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ","lamports":1000,"source":"web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"},"type":"transfer"},"program":"system","programId":"11111111111111111111111111111111","stackHeight":null}],"recentBlockhash":"7F3ptA9dwyosGYK2RMZneutNEfc6PruonnZcqVH35wyG"},"signatures":["3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC"]},"version":"legacy"} +``` + +## 8. getSignaturesForAddress + +Get a list of transaction signatures related to a specified account address. + +> This is very useful when monitoring an account. + +```ts +// Get the latest 3 transaction signatures for the account web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2 +const signatures = await connection.getSignaturesForAddress(new PublicKey("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"), { + limit: 3 + }); +console.log(`Latest 3 transaction signatures: ${JSON.stringify(signatures)}\n`); +``` + +``` +Latest 3 transaction signatures: [{"blockTime":1731241155,"confirmationStatus":"finalized","err":null,"memo":null,"signature":"5sFePYo4zAX2uiGmt1LmDBfoYDxXGhiix1of8K3DPD3Kua4fGStbMtKWvyUc1u1fdtrVS8DM51pgA9Us9GsaDRjm","slot":300566648},{"blockTime":1731232782,"confirmationStatus":"finalized","err":null,"memo":null,"signature":"3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC","slot":300547625},{"blockTime":1731232657,"confirmationStatus":"finalized","err":null,"memo":null,"signature":"4aZJwh2srekTB3w7VzF91rNv7oB1ZPMGwds1ohnivejHMUdSw7Eacp5kLkcChJuU2MmjewrusVNbHa2aCpjwTy6M","slot":300547340}] +``` + +## 9. getTokenAccountsByOwner + +Used to query all token accounts under a specific account. + +```ts +const tokenAccountsByOwner = await connection.getTokenAccountsByOwner(new PublicKey("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"), { + mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") +}, "confirmed"); +console.log(`Token accounts: ${JSON.stringify(tokenAccountsByOwner)}\n`); +``` + +``` +Token accounts: {"context":{"apiVersion":"2.0.3","slot":300580814},"value":[{"account":{"data":{"type":"Buffer","data":[198,250,122,243,190,219,173,58,61,101,243,106,171,201,116,49,177,187,228,194,210,246,224,228,124,166,2,3,69,47,93,97,13,255,221,17,24,19,199,35,78,149,150,80,234,75,209,227,110,41,100,154,108,37,103,158,230,205,202,31,47,49,116,137,128,132,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":18446744073709552000,"space":165},"pubkey":"HGtAdvmncQSk59mAxdh2M7GTUq1aB9WTwh7w7LwvbTBT"}]} +``` + +## 10. getTokenLargestAccounts + +Query the top 20 holders of a specific token. + +```ts +// Get the top 20 holders of the token with mint address Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump +const tokenLargestAccounts = await connection.getTokenLargestAccounts(new PublicKey("Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump")); +console.log(`Top 20 token holders: ${JSON.stringify(tokenLargestAccounts)}\n`); +``` + +``` +Top 20 token holders: {"context":{"apiVersion":"2.0.3","slot":300581319},"value":[{"address":"5hWv7FkSbyjyMNxKutWkA41azBFLkZNv3seLhZMeVR9f","amount":"152069454264255","decimals":6,"uiAmount":152069454.264255,"uiAmountString":"152069454.264255"},{"address":"EREWvGJSLVZ7cYqR9UBHags8Nu69UJWJTyUs5x1PxSVu","amount":"33554743151407","decimals":6,"uiAmount":33554743.151407,"uiAmountString":"33554743.151407"},{"address":"CsZcHJ9PgteaQfcNAsJhveM97THJ1erJYDCKxPv7zyoJ","amount":"23722304864271","decimals":6,"uiAmount":23722304.864271,"uiAmountString":"23722304.864271"},{"address":"BeJNuqkM7fLTQ9ayXxmRR2HcxwCtYdE5A83pDfkoK2ac","amount":"20153660767179","decimals":6,"uiAmount":20153660.767179,"uiAmountString":"20153660.767179"},{"address":"ECq3rtYeuGqjRYB5mnAQVkmnxEEhqZrCsj9iDXMLzMas","amount":"19542872794233","decimals":6,"uiAmount":19542872.794233,"uiAmountString":"19542872.794233"},{"address":"3eQwYYPvPRNT95ijnV7MhiuDf3UvFppgmbUfzGr3swGy","amount":"18960282274790","decimals":6,"uiAmount":18960282.27479,"uiAmountString":"18960282.27479"},{"address":"A9X8AbYgRUDgv76oJb3owJ9KZNQyMHYYoBUAcXYTQb4X","amount":"18385694793997","decimals":6,"uiAmount":18385694.793997,"uiAmountString":"18385694.793997"},{"address":"3tBpHjkbA2iPgLiynkh8aocx25cZw5giDsKgBMt18FQY","amount":"16192533170352","decimals":6,"uiAmount":16192533.170352,"uiAmountString":"16192533.170352"},{"address":"CKzAxaWfCvN2E2gsHZuh9ahkFURmWgrjcJmQJfS39JXw","amount":"15336629610352","decimals":6,"uiAmount":15336629.610352,"uiAmountString":"15336629.610352"},{"address":"AkRDFNqBny8QSWrm4hVHGz76AANHBYUySJ2FMMPJgFvc","amount":"14313037432834","decimals":6,"uiAmount":14313037.432834,"uiAmountString":"14313037.432834"},{"address":"7o9C3KFMinhVyCpAL18mjvWUAyNWECgfugoDEdEgVS3r","amount":"14278373348178","decimals":6,"uiAmount":14278373.348178,"uiAmountString":"14278373.348178"},{"address":"AgY1NbsCMaon6hjfcBMQjSaRyw8sUZNaoDht6H7Zw6GT","amount":"13601918495029","decimals":6,"uiAmount":13601918.495029,"uiAmountString":"13601918.495029"},{"address":"H1Qm7UNCdfhrbmjzzSKBN28xScZoBrU4CiTrQLfZwnTN","amount":"13212871578892","decimals":6,"uiAmount":13212871.578892,"uiAmountString":"13212871.578892"},{"address":"CLRVbDx6QkcSCGAkMSfmQuTFqpZun3tFRZHoCd7GV2MP","amount":"13045037329632","decimals":6,"uiAmount":13045037.329632,"uiAmountString":"13045037.329632"},{"address":"F6f91snaYJvioLtx5ESqKZTLxNuZn3erv78yBDvkP3kH","amount":"12234592572102","decimals":6,"uiAmount":12234592.572102,"uiAmountString":"12234592.572102"},{"address":"8D9v7JRy8uVLiqFcxQJjmPhdFttJcX6E5Kmn4WDFV3tM","amount":"11847710475369","decimals":6,"uiAmount":11847710.475369,"uiAmountString":"11847710.475369"},{"address":"Dvi82ZRJjey2eXV27U2Y8BGPbEkfq3B6t7pLsLCA4Ajh","amount":"11234459181997","decimals":6,"uiAmount":11234459.181997,"uiAmountString":"11234459.181997"},{"address":"HT1nud5TfKgnr2eG1bRB6eutRRNrHTS5uez4fFxL9txo","amount":"10980211007652","decimals":6,"uiAmount":10980211.007652,"uiAmountString":"10980211.007652"},{"address":"DrGFmy45YwcbUTZFn2XrKvVUTBbBY1jjGfMKGLFEkSmK","amount":"10536406834949","decimals":6,"uiAmount":10536406.834949,"uiAmountString":"10536406.834949"},{"address":"EkPJUGTRxEsDeLU7LbizUUgnA19GPzerUjLvEGAc9Zbi","amount":"10460309988199","decimals":6,"uiAmount":10460309.988199,"uiAmountString":"10460309.988199"}]} +``` + +## 11. getTokenSupply + +Get the supply of a token. + +```ts +// Get the supply of USDC +const supplyInfo = await connection.getTokenSupply(new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')); +console.log(`Total supply: ${supplyInfo.value.amount}\n`); +``` + +``` +Total supply: 3277049395067962 +``` + +## 12. getParsedProgramAccounts + +Batch retrieve all account information under a specific program. + +> This is very useful when obtaining all holders of a specific token. + +```ts +// Get all holders of the token with mint address Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump +const mintAddress = new PublicKey("Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump") +const accounts = await connection.getParsedProgramAccounts(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), { + filters: [ + { + dataSize: 165, // The data size of a Token account is 165 bytes + }, + { + memcmp: { + offset: 0, // Offset 0 indicates the position of the Token Mint address + bytes: mintAddress.toBase58(), + }, + }, + ], +}); + +// Only print the first 3 holders +console.log("First 3 accounts:", accounts.slice(0, 3)) + +``` + +``` +First 3 accounts: [ + { + account: { + data: [Object], + executable: false, + lamports: 2039280, + owner: [PublicKey [PublicKey(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)]], + rentEpoch: 18446744073709552000, + space: 165 + }, + pubkey: PublicKey [PublicKey(Avd9odZRLXLJTofuGSwPnUjTweniq7hae1fiyqeKMMiG)] { + _bn: + } + }, + { + account: { + data: [Object], + executable: false, + lamports: 2039280, + owner: [PublicKey [PublicKey(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)]], + rentEpoch: 18446744073709552000, + space: 165 + }, + pubkey: PublicKey [PublicKey(53P2PjAqKcVm7iE2ZKGF5BDWSxHY7EPvhQw7iTvMYjWn)] { + _bn: + } + }, + { + account: { + data: [Object], + executable: false, + lamports: 2039280, + owner: [PublicKey [PublicKey(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)]], + rentEpoch: 18446744073709552000, + space: 165 + }, + pubkey: PublicKey [PublicKey(7EyFQhPg4S61U1FHnbL7BK1pnhFFyGLGXc6C99RGJCei)] { + _bn: + } + } +] +``` \ No newline at end of file diff --git a/en/04-get/index.ts b/en/04-get/index.ts new file mode 100644 index 0000000..edb4ac1 --- /dev/null +++ b/en/04-get/index.ts @@ -0,0 +1,78 @@ +import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +async function main() { + + // 1. getSlot + const slot = await connection.getSlot(); + console.log(`Current slot: ${slot}\n`); + + // 2. getBalance + // const balance = await connection.getBalance(new PublicKey("CXPeim1wQMkcTvEHx9QdhgKREYYJD8bnaCCqPRwJ1to1")); + // console.log(`J1to1 balance: ${balance / LAMPORTS_PER_SOL} SOL\n`); + + // 3. getTokenAccountBalance + // const tokenAccountBalance = await connection.getTokenAccountBalance(new PublicKey("HGtAdvmncQSk59mAxdh2M7GTUq1aB9WTwh7w7LwvbTBT")); + // console.log(`Token account balance: ${JSON.stringify(tokenAccountBalance)}\n`); + + // 4. getFirstAvailableBlock + // const firstAvailableBlock = await connection.getFirstAvailableBlock(); + // console.log(`First available block: ${firstAvailableBlock}\n`); + + // 5. getLatestBlockhash + // const latestBlockhash = await connection.getLatestBlockhash(); + // console.log(`Latest blockhash: ${latestBlockhash.blockhash}\n`); + + // 6. getParsedAccountInfo + // const parsedAccountInfo = await connection.getParsedAccountInfo(new PublicKey('8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj'), "confirmed"); + // console.log("Parsed account info:", parsedAccountInfo) + + // 7. getParsedTransaction + // const parsedTransaction = await connection.getParsedTransaction('3Vfp5qPhF14bNb2jLtTccabCDbHUmxqtXerUvPEjKb6RpJ8jU3H9M9JgcUbDPtgesB3WFP9M8VZTzECgBavnjxaC', { + // commitment: "confirmed", + // maxSupportedTransactionVersion: 0 + // }); + // console.log(`Parsed transaction: ${JSON.stringify(parsedTransaction)}\n`); + + // 8. getSignaturesForAddress + // const signatures = await connection.getSignaturesForAddress(new PublicKey("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"), { + // limit: 3 + // }); + // console.log(`Recent 3 transaction signatures: ${JSON.stringify(signatures)}\n`); + + // 9. getTokenAccountsByOwner + // const tokenAccountsByOwner = await connection.getTokenAccountsByOwner(new PublicKey("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"), { + // mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") + // }, "confirmed"); + // console.log(`Token accounts: ${JSON.stringify(tokenAccountsByOwner)}\n`); + + // 10. getTokenLargestAccounts + // const tokenLargestAccounts = await connection.getTokenLargestAccounts(new PublicKey("Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump")); + // console.log(`20 largest token holder accounts: ${JSON.stringify(tokenLargestAccounts)}\n`); + + // 11. getTokenSupply + // const supplyInfo = await connection.getTokenSupply(new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')); + // console.log(`Total supply: ${supplyInfo.value.amount}\n`); + + // 12. getParsedProgramAccounts + // const mintAddress = new PublicKey("Dp4fXozKtwgK1cL5KQeeNbuAgFpJtY3FbAvL8JrWpump") + // const accounts = await connection.getParsedProgramAccounts(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), { + // filters: [ + // { + // dataSize: 165, // The data size of a Token account is 165 bytes + // }, + // { + // memcmp: { + // offset: 0, // Offset 0 indicates the position of the Token Mint address + // bytes: mintAddress.toBase58(), + // }, + // }, + // ], + // }); + // console.log("First 3 accounts:", accounts.slice(0, 3)) + +} + +main(); \ No newline at end of file diff --git a/en/05-on/README.md b/en/05-on/README.md new file mode 100644 index 0000000..0a5798a --- /dev/null +++ b/en/05-on/README.md @@ -0,0 +1,49 @@ +# Reading On-Chain Data (Part 2): `on` Subscriptions + +This section will introduce you to some common methods for subscribing to on-chain data and their use cases. + +Unlike single reads of on-chain data, subscriptions allow RPC nodes to push data to the client in a streaming manner. These methods also belong to the `Connection` class and have method names that start with `on`. + +First, let's create an RPC connection: + +```ts +import { Connection, PublicKey } from '@solana/web3.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +``` + +## 1. onAccountChange and onProgramAccountChange + +These are used to listen for real-time changes in the state of specific accounts. When the balance or stored data of an account changes, a callback function is triggered, providing the updated account information. + +> This is very useful for wallet monitoring. + +```ts +// Listen for changes to the account orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8 +connection.onAccountChange(new PublicKey("orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8"), (accountInfo) => { + console.log(`Account change: ${JSON.stringify(accountInfo)}\n`); +}); +``` + +``` +Account change: {"lamports":54656348509,"data":{"type":"Buffer","data":[]},"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":18446744073709552000,"space":0} +``` + +`onProgramAccountChange` is similar, but it subscribes to the state of program accounts. + +## 2. onLogs + +This is used to listen for logs on the network in real-time, and you can also specify logs under a certain account. + +> This is useful for monitoring new liquidity pool creations and wallet buy/sell actions. + +```ts +// Listen for logs from raydium v4 +connection.onLogs(new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), (logs) => { + console.log(`Logs: ${JSON.stringify(logs)}\n`); +}); +``` + +``` +Logs: {"signature":"5FziCd9SRzEJyXcxRotJqamGdcTfdMKiWB4GPvg2HiWFpzzyvnBGTaV6Vd8KANS85yHjszE61BxFc1gQQgSdATSg","err":null,"logs":["Program ComputeBudget111111111111111111111111111111 invoke [1]","Program ComputeBudget111111111111111111111111111111 success","Program 6rHBDckrDivyp5UarGvCqobhtdfuBf2p7E42zXNbKBGm invoke [1]","Program log: Instruction: Finish","Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [2]","Program log: ray_log: A0cBmjWFAAAAbK8wbAAAAAACAAAAAAAAAEcBmjWFAAAA/2uOg3AUAADPqGwiEQAAAOq+oWwAAAAA","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program log: Instruction: Transfer","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 101749 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program log: Instruction: Transfer","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 94123 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 32059 of 120402 compute units","Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]","Program log: Instruction: CloseAccount","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2916 of 85643 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]","Program log: Instruction: CloseAccount","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3014 of 78745 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [2]","Program log: Create","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program log: Instruction: GetAccountDataSize","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 58189 compute units","Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program 11111111111111111111111111111111 invoke [3]","Program 11111111111111111111111111111111 success","Program 11111111111111111111111111111111 invoke [3]","Program 11111111111111111111111111111111 success","Program log: Initialize the associated token account","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program log: Instruction: InitializeImmutableOwner","Program log: Please upgrade to SPL Token 2022 for immutable owner support","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 50213 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]","Program log: Instruction: InitializeAccount3","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3158 of 46329 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 22297 of 65164 compute units","Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success","Program 6rHBDckrDivyp5UarGvCqobhtdfuBf2p7E42zXNbKBGm consumed 108080 of 149850 compute units","Program 6rHBDckrDivyp5UarGvCqobhtdfuBf2p7E42zXNbKBGm success"]} +``` \ No newline at end of file diff --git a/en/05-on/index.ts b/en/05-on/index.ts new file mode 100644 index 0000000..0cb4e3e --- /dev/null +++ b/en/05-on/index.ts @@ -0,0 +1,20 @@ +import { Connection, PublicKey } from '@solana/web3.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +async function main() { + + // 1. onAccountChange + // connection.onAccountChange(new PublicKey("orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8"), (accountInfo) => { + // console.log(`Account change: ${JSON.stringify(accountInfo)}\n`); + // }); + + // 2. onLogs + connection.onLogs(new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), (logs) => { + console.log(`Logs: ${JSON.stringify(logs)}\n`); + }); + +} + +main(); \ No newline at end of file diff --git a/en/06-send/README.md b/en/06-send/README.md new file mode 100644 index 0000000..60654c5 --- /dev/null +++ b/en/06-send/README.md @@ -0,0 +1,74 @@ +# Writing On-Chain Data: `send` Sending Transactions + +This section will guide you through several common methods for sending transactions, which are functionally similar: `sendAndConfirmTransaction`, `sendRawTransaction`, and `sendEncodedTransaction`. + +## 1. sendAndConfirmTransaction + +Sends a transaction and waits for its confirmation, with an automatic confirmation mechanism. Suitable for scenarios where you want to simplify transaction confirmation. + +```ts +const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet], { + skipPreflight: false + }); +console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +``` + +After running with `npx esrun 06-send/index.ts`, you can view the simulated transaction results and whether the transaction was sent successfully. + +``` +Simulated transaction result: { + context: { apiVersion: '2.0.3', slot: 300771879 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 150 + } +} +Transaction sent: https://solscan.io/tx/4W4QvBfAzezyxatAbE53awibDytLkZmcagnBihz8QSaqhMTJFN5AoQjfxggm7PGQgYmTvTHWhmDSsi4JEn4wCFwX +``` + +## 2. sendRawTransaction + +Sends a signed and serialized transaction. + +```ts +const { blockhash } = await connection.getLatestBlockhash(); +transaction.recentBlockhash = blockhash; +transaction.feePayer = fromWallet.publicKey; +transaction.sign(fromWallet); +const rawTransaction = transaction.serialize(); + +const signature = await connection.sendRawTransaction(rawTransaction, { + skipPreflight: false +}); +console.log("Transaction signature:", signature); +``` + +``` +Transaction signature: 3CuP3PpSMMknoB88kWEzAC6dxTAYx1x5oo9KjFSzaCydZRcSdpogBdLLJbKEVHp8nmPfyxB3UhUQtnM6YNFNsA6A +``` + +## 3. sendEncodedTransaction + +Sends base64 encoded transaction data, which has better compatibility. + +```ts +const base64Transaction = rawTransaction.toString('base64'); +const signature = await connection.sendEncodedTransaction(base64Transaction, { + skipPreflight: false +}); +console.log("Transaction signature:", signature); +``` + +``` +Transaction signature: 4NnavZedvvx5s7KTYnuVcvbZiV4dsUDUswJBe11E7FV2txcCYM8ypjbDmvHzWFvqKf9t3RZLkEo7Ek2HxoDyCcV8 +``` + +> Note: If you set skipPreflight to true, it means you will skip the simulation process before sending the transaction, which can speed up submission but may also lead to transaction failure, resulting in the loss of transaction fees. \ No newline at end of file diff --git a/en/06-send/index.ts b/en/06-send/index.ts new file mode 100644 index 0000000..f28cac4 --- /dev/null +++ b/en/06-send/index.ts @@ -0,0 +1,72 @@ +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + sendAndConfirmTransaction, +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +// Import wallet locally +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); + +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + // const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + // console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + + // Send transaction + // 1. sendAndConfirmTransaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet], { + skipPreflight: false + }); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + + // const { blockhash } = await connection.getLatestBlockhash(); + // transaction.recentBlockhash = blockhash; + // transaction.feePayer = fromWallet.publicKey; + // transaction.sign(fromWallet); + // const rawTransaction = transaction.serialize(); + + // 2. sendRawTransaction + // const signature = await connection.sendRawTransaction(rawTransaction, { + // skipPreflight: false + // }) + // console.log("Transaction signature:", signature) + + // 3. sendEncodedTransaction + // const base64Transaction = rawTransaction.toString('base64'); + // const signature = await connection.sendEncodedTransaction(base64Transaction, { + // skipPreflight: false + // }); + // console.log("Transaction signature:", signature) + +} + +main(); \ No newline at end of file diff --git a/en/07-cu/README.md b/en/07-cu/README.md new file mode 100644 index 0000000..f7f922c --- /dev/null +++ b/en/07-cu/README.md @@ -0,0 +1,185 @@ +# Adding Priority Fees + +Priority fees are additional fees paid on top of the base transaction fee (5000 Lamports). By adding a priority fee to a transaction, you can increase the priority of the transaction being processed when block space is limited. + +The minimum component for calculating transaction fees on Solana is `Compute Units (CU)`, which measures the computational resources consumed by a transaction. + +The calculation method for priority fees is `Number of CUs * Price per CU`, where the price is in `microLamports`. + +> 1 Lamport = 10^6 microLamports + +Therefore, the priority fee for a transaction should be divided into two parts: `Number of CUs` and `CU Price`. We can customize our transaction's priority fees through the Compute Budget program instructions. + +## Compute Budget Program Instructions + +This section will explain two methods for setting priority fees: `setComputeUnitPrice` and `setComputeUnitLimit`. + +First, create an RPC connection and import our wallet: + +```ts +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + ComputeBudgetProgram, + sendAndConfirmTransaction +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +``` + +### setComputeUnitPrice + +By default, the allocation of transaction CU numbers is `200000 * Number of Instructions`. + +> Note that Compute Budget program instructions are not counted in the number of instructions here. + +By setting the price per CU to 5 microLamports using `setComputeUnitPrice`, we can see that [this transaction, which contains only one transfer instruction](https://solscan.io/tx/34eohoyTp2oZ1jtFNtcEUp9oe2QfRf5HRarexCbKnUm93ga3sGjP8Aduwd8xcbRrZk9HNdRJ9rqWZ8peGhruPfuK), has a priority fee of 0.000000001 SOL, i.e., `200000 * 1 * 5 = 1 Lamport`. + +```ts +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // CU price + const computeUnitPriceInstruction = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 5 + }); + transaction.add(computeUnitPriceInstruction); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction1 = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction1); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); +``` + +When running with `npx esrun 07-cu/index.ts`, the output should be as follows: + +``` +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301579053 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 300 + } +} +Transaction sent: https://solscan.io/tx/34eohoyTp2oZ1jtFNtcEUp9oe2QfRf5HRarexCbKnUm93ga3sGjP8Aduwd8xcbRrZk9HNdRJ9rqWZ8peGhruPfuK +``` + +Here are a few other transaction examples: + +- Price per CU is 1 microLamports, 1 transfer instruction. Due to being less than 1 Lamport, it is processed as 1 Lamport: [Priority fee is 1 Lamport](https://solscan.io/tx/5WxZ9uST4Raz3fyCJyodLcx3Ruyy2JYPvJa7kD28ehn9VquPkV6jm6pzAGFGt8c1fYF7yhFpptTTJCX6PYsJr8i9) +- Price per CU is 25 microLamports, 1 transfer instruction: [Priority fee is 5 Lamports](https://solscan.io/tx/EWgoUVMGLNEzAoGiY79NWzhzFHFk8bw5ivGXBE82afsCL5E3o71jvVQKFPS3f1NvggdMQGc6naWHLphFS2oqaYX) +- Price per CU is 5 microLamports, 2 transfer instructions: [Priority fee is 2 Lamports](https://solscan.io/tx/4SbD6b1aFG4fXErzFcGzx6xr3RXFVB66Xm2yXwe2PP4qsmzRQKfzeibeJuWPtrEccnsky2MW9wm3UtjrRfJL1YsE) + +### setComputeUnitLimit + +In fact, the computational resource consumption of a simple transfer instruction is only 150 CUs, while the default CU allocation per instruction is 200000, which often leads to additional priority fee expenditures. + +Therefore, we can determine the CU limit for our transaction using `setComputeUnitLimit`. + +> The maximum CU allocation for each transaction is `1400000`. + +Since a priority fee of less than 1 Lamport will be processed as 1 Lamport, for estimation, we take the CU limit as 500 (the actual cost for 3 instructions is 450). If we set the CU price to 4000 microLamports, the priority fee should be 2 Lamports, [see this transaction here](https://solscan.io/tx/41APCjw2ifHkqV3Ha7S7Q1LADb9S396acsCqErdk6Tp3xcCMLge1FueRnj4dfTSbhgeA9DBvfPNRJxoZuYEqCQUS). + +```ts +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // CU price + const computeUnitPriceInstruction = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 4000 + }); + transaction.add(computeUnitPriceInstruction); + + // CU limit + const computeUnitLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ + units: 500, + }); + transaction.add(computeUnitLimitInstruction); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction1 = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction1); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); +``` + +When running again, the output should be as follows: + +``` +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301580071 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 450 + } +} +Transaction sent: https://solscan.io/tx/41APCjw2ifHkqV3Ha7S7Q1LADb9S396acsCqErdk6Tp3xcCMLge1FueRnj4dfTSbhgeA9DBvfPNRJxoZuYEqCQUS +``` + +--- + +## Summary + +By using the `setComputeUnitPrice` and `setComputeUnitLimit` instructions, we can flexibly define our own priority fees in practical applications. Generally, it is necessary to estimate the CU consumption of your transactions and fix this value to dynamically adjust the CU price parameters, ensuring that your transaction's priority fee changes with the network's priority fee levels. \ No newline at end of file diff --git a/en/07-cu/index.ts b/en/07-cu/index.ts new file mode 100644 index 0000000..480afb5 --- /dev/null +++ b/en/07-cu/index.ts @@ -0,0 +1,66 @@ +import { + Connection, + PublicKey, + Keypair, + Transaction, + SystemProgram, + ComputeBudgetProgram, + sendAndConfirmTransaction +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +// Local import wallet +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); + +async function main() { + + // Create transaction + const transaction = new Transaction(); + + // CU price + const computeUnitPriceInstruction = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 5 + }); + transaction.add(computeUnitPriceInstruction); + + // CU limit + // const computeUnitLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ + // units: 500, + // }); + // transaction.add(computeUnitLimitInstruction); + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Add transfer instruction + const instruction1 = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + transaction.add(instruction1); + + // // Add transfer instruction + // const instruction2 = SystemProgram.transfer({ + // fromPubkey: fromWallet.publicKey, + // toPubkey: toAddress, + // lamports: 1000, // 1000 lamports + // }); + // transaction.add(instruction2); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction, [fromWallet]); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [fromWallet]); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); \ No newline at end of file diff --git a/en/08-v0/README.md b/en/08-v0/README.md new file mode 100644 index 0000000..c9a0c4c --- /dev/null +++ b/en/08-v0/README.md @@ -0,0 +1,373 @@ +# `v0` Transactions + +Versioned Transactions, also known as `v0` transactions, are a new type of transaction introduced during the Solana update process. Due to the limitations on the number of accounts in traditional (legacy) transactions, `v0` transactions introduce the Address Lookup Tables feature to compress the number of accounts, increasing the limit from 35 to 64 accounts. + +> `v0` transactions are widely used in on-chain arbitrage scenarios. + +This section will explain how to create `v0` transactions and how to use Address Lookup Tables. + +## Creating `v0` Transactions + +Here is an example of creating a `v0` transaction. + +> Unless otherwise specified, it is recommended that all transactions use the `v0` type. + +```ts +import { + Connection, + PublicKey, + Keypair, + TransactionMessage, + VersionedTransaction, + SystemProgram, +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); + +// Import wallet locally +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); +``` + +```ts +async function main() { + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: fromWallet.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [instruction], // Instruction array + }).compileToV0Message(); + + // Create and sign v0 transaction + const transaction = new VersionedTransaction(messageV0); + transaction.sign([fromWallet]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); +``` + +By running `npx esrun 08-v0/index.ts`, the output is as follows: + +``` +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301586586 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 150 + } +} +Transaction sent: https://solscan.io/tx/5BLjkVYjkLHa7rz7616r6Nx4fbMYe4Y3mBj6ucihrB7hXmjfe59V16MHfPsVYhECZs8qBU6n39kzxLQgm89pQ8k1 +``` + +You can [view this transaction](https://solscan.io/tx/5BLjkVYjkLHa7rz7616r6Nx4fbMYe4Y3mBj6ucihrB7hXmjfe59V16MHfPsVYhECZs8qBU6n39kzxLQgm89pQ8k1) version in the blockchain explorer. + +![](../../img/08-01.png) + +## Address Lookup Tables + +By interacting with the `AddressLookupTableProgram`, you can create your own address lookup table and introduce your lookup table into transactions. + +### Creating an ALT + +```ts +import { + Connection, + PublicKey, + Keypair, + TransactionMessage, + VersionedTransaction, + SystemProgram, + AddressLookupTableProgram +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +// const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); +const connection = new Connection("https://chrissy-w0sbco-fast-mainnet.helius-rpc.com", "confirmed"); + +// Import wallet locally +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const payer = Keypair.fromSecretKey(secretKey); +``` + +```ts +async function createALT() { + + // Get current slot + const slot = await connection.getSlot("confirmed"); + + // Create ALT + const [lookupTableInstruction, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: payer.publicKey, + payer: payer.publicKey, + recentSlot: slot, + }); + + console.log("Lookup table address:", lookupTableAddress.toBase58()); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [lookupTableInstruction], // Instruction array + }).compileToV0Message(); + + // Create and sign v0 transaction + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +// Create ALT +createALT(); +``` + +By running `npx esrun 08-v0/alt.ts`, the output is as follows: + +``` +ALT account address 2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5 +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301694685 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program AddressLookupTab1e1111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 invoke [2]', + 'Program 11111111111111111111111111111111 success', + 'Program 11111111111111111111111111111111 invoke [2]', + 'Program 11111111111111111111111111111111 success', + 'Program 11111111111111111111111111111111 invoke [2]', + 'Program 11111111111111111111111111111111 success', + 'Program AddressLookupTab1e1111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 1200 + } +} +Transaction sent: https://solscan.io/tx/5jeF2fY2B83ETueuzcdF5bjXpB949gW9FxMEietnpCDgW7LFgsKFxcnLDYxcE1RDBKcPjMYw3sJQLKv2TxjPajCT +``` + +Thus, we have created our own address lookup table with the address `2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5`. + +### Adding Account Addresses to ALT + +```ts +async function addAddresses() { + + const lookupTableAddress = new PublicKey('2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5') + + // Add account to ALT + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + lookupTable: lookupTableAddress, + payer: payer.publicKey, + authority: payer.publicKey, + addresses: [ + payer.publicKey, + new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'), + SystemProgram.programId, // + ], + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [extendInstruction], // Instruction array + }).compileToV0Message(); + + // Create and sign v0 transaction + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + +} + +addAddresses() +``` + +``` +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301695975 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program AddressLookupTab1e1111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 invoke [2]', + 'Program 11111111111111111111111111111111 success', + 'Program AddressLookupTab1e1111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 900 + } +} +Transaction sent: https://solscan.io/tx/ZRM6NDdtFkH4dRxBNe3r4mEg8yNF87UCs8UE2vycec1Y88XcDHWVbU6Wa7den3a9o6EwzdVFQr6PvW2i19Qv5FF +``` + +You can view the recently added account addresses in our address lookup table account. + +![](../../img/08-02.png) + +### Using ALT in `v0` Transactions + +```ts +async function transfer() { + + const lookupTableAddress = new PublicKey('2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5') + + // Get ALT + const ALT = await connection.getAddressLookupTable(lookupTableAddress); + const lookupTableAccount = ALT.value; + if (!ALT.value) { + throw new Error("lookupTableAccount does not exist"); + } + console.log('lookupTableAccount:', lookupTableAccount) + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [instruction], // Instruction array + }).compileToV0Message([lookupTableAccount]); + + // Create and sign v0 transaction + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + +} + +transfer() +``` + +``` +lookupTableAccount: AddressLookupTableAccount { + key: PublicKey [PublicKey(2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5)] { + _bn: + }, + state: { + deactivationSlot: 18446744073709551615n, + lastExtendedSlot: 301695984, + lastExtendedSlotStartIndex: 0, + authority: PublicKey [PublicKey(web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2)] { + _bn: + }, + addresses: [ + [PublicKey [PublicKey(web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2)]], + [PublicKey [PublicKey(buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ)]], + [PublicKey [PublicKey(11111111111111111111111111111111)]] + ] + } +} +Simulation result: { + context: { apiVersion: '2.0.3', slot: 301701358 }, + value: { + accounts: null, + err: null, + innerInstructions: null, + logs: [ + 'Program 11111111111111111111111111111111 invoke [1]', + 'Program 11111111111111111111111111111111 success' + ], + replacementBlockhash: null, + returnData: null, + unitsConsumed: 150 + } +} +Transaction sent: https://solscan.io/tx/4LwygRtiF9ZCrbGKoh8MEzmxowaRHPaDc1nsinkv72uXU2cUCuZ8YskBBgsvbBEMZ5Pqpf6C6WcXtCkqAuLZand1 +``` + +![](../../img/08-03.png) + +Call `getParsedTransaction` to retrieve this `v0` transaction as follows: + +```ts +async function parseTx() { + + const parsedTransaction = await connection.getParsedTransaction('4LwygRtiF9ZCrbGKoh8MEzmxowaRHPaDc1nsinkv72uXU2cUCuZ8YskBBgsvbBEMZ5Pqpf6C6WcXtCkqAuLZand1', { + commitment: "confirmed", + maxSupportedTransactionVersion: 0 + }); + console.log(`Parsed v0 transaction: ${JSON.stringify(parsedTransaction)}\n`); + +} + +parseTx() +``` + +``` +Parsed v0 transaction: {"blockTime":1731742689,"meta":{"computeUnitsConsumed":150,"err":null,"fee":5000,"innerInstructions":[],"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"postBalances":[5770640,1,2018400],"postTokenBalances":[],"preBalances":[5776640,1,2017400],"preTokenBalances":[],"rewards":[],"status":{"Ok":null}},"slot":301701368,"transaction":{"message":{"accountKeys":[{"pubkey":"web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2","signer":true,"source":"transaction","writable":true},{"pubkey":"11111111111111111111111111111111","signer":false,"source":"transaction","writable":false},{"pubkey":"buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ","signer":false,"source":"lookupTable","writable":true}],"addressTableLookups":[{"accountKey":"2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5","readonlyIndexes":[],"writableIndexes":[1]}],"instructions":[{"parsed":{"info":{"destination":"buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ","lamports":1000,"source":"web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2"},"type":"transfer"},"program":"system","programId":"11111111111111111111111111111111","stackHeight":null}],"recentBlockhash":"DcQMezPzouNnbrHufbhrpjFftMxVpDKX4vwCGc2NQHKZ"},"signatures":["4LwygRtiF9ZCrbGKoh8MEzmxowaRHPaDc1nsinkv72uXU2cUCuZ8YskBBgsvbBEMZ5Pqpf6C6WcXtCkqAuLZand1"]},"version":0} +``` diff --git a/en/08-v0/alt.ts b/en/08-v0/alt.ts new file mode 100644 index 0000000..4de4e60 --- /dev/null +++ b/en/08-v0/alt.ts @@ -0,0 +1,159 @@ +import { + Connection, + PublicKey, + Keypair, + TransactionMessage, + VersionedTransaction, + SystemProgram, + AddressLookupTableProgram +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +// Local wallet import +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const payer = Keypair.fromSecretKey(secretKey); + +async function createALT() { + + // Get current slot + const slot = await connection.getSlot("confirmed"); + + // Create ALT + const [lookupTableInstruction, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: payer.publicKey, + payer: payer.publicKey, + recentSlot: slot, + }); + + console.log("lookup table address:", lookupTableAddress.toBase58()); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [lookupTableInstruction], // Instruction array + }).compileToV0Message(); + + // Create v0 transaction and sign + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +async function addAddresses() { + + const lookupTableAddress = new PublicKey('2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5') + + // Add accounts to ALT + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + lookupTable: lookupTableAddress, + payer: payer.publicKey, + authority: payer.publicKey, + addresses: [ + payer.publicKey, + new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'), + SystemProgram.programId, // + ], + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [extendInstruction], // Instruction array + }).compileToV0Message(); + + // Create v0 transaction and sign + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + +} + +async function transfer() { + + const lookupTableAddress = new PublicKey('2qqXrZZSG9naivqMyWHHUDRFVNh3YthsTbN5EPU8Poo5') + + // Get ALT + const ALT = await connection.getAddressLookupTable(lookupTableAddress); + const lookupTableAccount = ALT.value; + if (!ALT.value) { + throw new Error("lookupTableAccount does not exist"); + } + console.log('lookupTableAccount:', lookupTableAccount) + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [instruction], // Instruction array + }).compileToV0Message([lookupTableAccount]); + + // Create v0 transaction and sign + const transaction = new VersionedTransaction(messageV0); + transaction.sign([payer]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); + +} + +async function parseTx() { + + const parsedTransaction1 = await connection.getParsedTransaction('4LwygRtiF9ZCrbGKoh8MEzmxowaRHPaDc1nsinkv72uXU2cUCuZ8YskBBgsvbBEMZ5Pqpf6C6WcXtCkqAuLZand1', { + commitment: "confirmed", + maxSupportedTransactionVersion: 0 + }); + console.log(`Parsed v0 transaction: ${JSON.stringify(parsedTransaction1)}\n`); + +} + +// Create ALT +// createALT(); + +// Add accounts to ALT +// addAddresses(); + +// Transfer using ALT +// transfer(); + +// Get parsed v0 transaction +parseTx(); \ No newline at end of file diff --git a/en/08-v0/index.ts b/en/08-v0/index.ts new file mode 100644 index 0000000..153c8a9 --- /dev/null +++ b/en/08-v0/index.ts @@ -0,0 +1,53 @@ +import { + Connection, + PublicKey, + Keypair, + TransactionMessage, + VersionedTransaction, + SystemProgram, +} from '@solana/web3.js'; +import fs from "fs"; + +// Create RPC connection +// const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +// Local wallet import +// const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("wallet.json"))); +const fromSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync("web3xFMwEPrc92NeeXdAigni95NDnnd2NPuajTirao2.json"))); +const fromWallet = Keypair.fromSecretKey(fromSecretKey); + +async function main() { + + // Target address + const toAddress = new PublicKey('buffaAJKmNLao65TDTUGq8oB9HgxkfPLGqPMFQapotJ'); + + // Transfer instruction + const instruction = SystemProgram.transfer({ + fromPubkey: fromWallet.publicKey, + toPubkey: toAddress, + lamports: 1000, // 1000 lamports + }); + + // Create v0 message + const { blockhash } = await connection.getLatestBlockhash(); + const messageV0 = new TransactionMessage({ + payerKey: fromWallet.publicKey, + recentBlockhash: blockhash, // Recent block hash + instructions: [instruction], // Instruction array + }).compileToV0Message(); + + // Create v0 transaction and sign + const transaction = new VersionedTransaction(messageV0); + transaction.sign([fromWallet]); + + // Simulate transaction + const simulateResult = await connection.simulateTransaction(transaction); + console.log("Simulation result: ", simulateResult); + + // Send transaction + const signature = await connection.sendTransaction(transaction); + console.log(`Transaction sent: https://solscan.io/tx/${signature}`); +} + +main(); \ No newline at end of file diff --git a/en/09-buffer/README.md b/en/09-buffer/README.md new file mode 100644 index 0000000..3bbf09c --- /dev/null +++ b/en/09-buffer/README.md @@ -0,0 +1,64 @@ +# Buffer Parsing + +Buffer parsing essentially involves interpreting the raw binary data stored on-chain. + +In general, we often do not know the structure of each segment of binary data, and we usually only need a specific field from this binary data. Therefore, this section will introduce a manual method for parsing on-chain binary data, using the example of obtaining the WSOL price from the Raydium CLMM WSOL/USDC liquidity pool, with the address `8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj`. + +> Obtaining the liquidity pool price is commonly used in arbitrage and liquidity mining scenarios. + +## Parsing the USDC Relative Price of WSOL in the WSOL/USDC Liquidity Pool + +In the Raydium CLMM protocol liquidity pool, the token price is explicitly stored in the account data of the liquidity pool account address, as shown below: + +![](../../img/09-01.png) + +Here, we assume that we do not know the structure of the stored data and will use a manual parsing method to obtain this price. + +The data type of `sqrtPriceX64` is `u128`, occupying 16 bytes. Therefore, we only need to obtain the starting offset of `sqrtPriceX64` and read 16 bytes for parsing. + +The lengths of other data types are marked in the image, where `u8` is 1, `pubkey` is 32, and `u16` is 2. + +> Note: In Solana's account data, the initial 8 bytes are used for the prefix identifier (discriminator). + +Thus, the starting offset of `sqrtPriceX64` should be $8 + 1 + 32*7 + 1 + 1 + 2 + 16 = 253$ + +![](../../img/09-02.png) + +```ts +import { Connection, PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); + +async function main() { + + const poolAccountPublicKey = new PublicKey('8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj'); + const accountInfo = await connection.getAccountInfo(poolAccountPublicKey); + const dataBuffer = accountInfo?.data; + if (!dataBuffer) { + throw new Error("Account data not found"); + } + console.log(dataBuffer) + + const offset = 253 + const sqrtPriceX64Buffer = dataBuffer.slice(offset, offset + 16); // Read 16 bytes + const sqrtPriceX64Value = new BN(sqrtPriceX64Buffer, 'le'); // Create BN instance using little-endian byte order + console.log(`sqrtPriceX64Value at offset ${offset}:`, sqrtPriceX64Value.toString()); + + // Calculate price + const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); + const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); + const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; + console.log(`WSOL Price:`, price.toString()) +} + +main(); +``` + +Running with `npx esrun 09-buffer/index.ts` should output as follows: + +``` + +sqrtPriceX64Value at offset 253: 8622437757683733036 +WSOL Price: 218.4845296506469 \ No newline at end of file diff --git a/en/09-buffer/index.ts b/en/09-buffer/index.ts new file mode 100644 index 0000000..213b907 --- /dev/null +++ b/en/09-buffer/index.ts @@ -0,0 +1,30 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; + +// Create RPC connection +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +async function main() { + + const poolAccountPublicKey = new PublicKey('8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj'); + const accountInfo = await connection.getAccountInfo(poolAccountPublicKey); + const dataBuffer = accountInfo?.data; + if (!dataBuffer) { + throw new Error("Account data not found"); + } + console.log(dataBuffer) + + const offset = 253 + const sqrtPriceX64Buffer = dataBuffer.slice(offset, offset + 16); // Read 16 bytes + const sqrtPriceX64Value = new BN(sqrtPriceX64Buffer, 'le'); // Create BN instance using little-endian byte order + console.log(`sqrtPriceX64Value at offset ${offset}:`, sqrtPriceX64Value.toString()); + + // Calculate price + const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); + const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); + const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; + console.log(`WSOL price:`, price.toString()) +} + +main(); \ No newline at end of file diff --git a/en/example-01-subWallet/README.md b/en/example-01-subWallet/README.md new file mode 100644 index 0000000..a9bb007 --- /dev/null +++ b/en/example-01-subWallet/README.md @@ -0,0 +1,9 @@ +# Listening to Wallet + +This example combines the `onAccountChange` and `getSignaturesForAddress` methods to subscribe to changes in a specified account and retrieve the latest transactions for that account. + +After obtaining the latest transactions, notifications will be sent via a Telegram bot. + +Run it using `npx esrun example-01-subWallet/index.ts`. + +![](../../img/example-01-01.png) \ No newline at end of file diff --git a/en/example-01-subWallet/index.ts b/en/example-01-subWallet/index.ts new file mode 100644 index 0000000..8000dda --- /dev/null +++ b/en/example-01-subWallet/index.ts @@ -0,0 +1,43 @@ +import { + Connection, + PublicKey +} from '@solana/web3.js'; +import axios from 'axios'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); +const publicKey = new PublicKey('orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8'); +const botToken = ''; +const chatId = ''; + +async function sendMessage(message) { + const url = `https://api.telegram.org/bot${botToken}/sendMessage`; + + try { + await axios.post(url, { + chat_id: chatId, + text: message, + parse_mode: 'HTML', + disable_web_page_preview: true, + }); + + } catch (error) { + console.error('Error sending message:', error.message); + } +} + +// async function test() { +// const sig = await connection.getSignaturesForAddress(publicKey, {limit: 1}, 'confirmed'); +// await sendMessage(`New transaction!\n\nhttps://solscan.io/tx/${sig[0].signature}`) +// } + +// test(); + +connection.onAccountChange( + publicKey, + async () => { + const sig = await connection.getSignaturesForAddress(publicKey, {limit: 1}, 'confirmed'); + await sendMessage(`New transaction!\n\nhttps://solscan.io/tx/${sig[0].signature}`) + }, + 'confirmed' +); \ No newline at end of file diff --git a/en/example-02-subNewPool/README.md b/en/example-02-subNewPool/README.md new file mode 100644 index 0000000..033cfee --- /dev/null +++ b/en/example-02-subNewPool/README.md @@ -0,0 +1,9 @@ +# Listening for New Raydium Liquidity Pool Creation + +This example uses the `onLogs` method to subscribe to and filter the creation of new liquidity pools at the Raydium v4 address `675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8`. + +Run it using `npx esrun example-02-subNewPool/index.ts`. + +``` +New liquidity pool created: https://solscan.io/tx/28XF272z2ipWMfQ2dtdJgRFiSuJojCaxKknXRyekbzKd1ygtrjciikTjcvTCDeHzTJfe9hPvnkDQuMztaRWRdbGa +New liquidity pool created: https://solscan.io/tx/4pMQnmpnoGcaSmH1apS91Gknj94vedDkdfaAQKMeaq3GqU6WFq5o5uySRTCTCQcaQqpL9F5Cjw3ncrtnqEdMHa4x \ No newline at end of file diff --git a/en/example-02-subNewPool/index.ts b/en/example-02-subNewPool/index.ts new file mode 100644 index 0000000..5855082 --- /dev/null +++ b/en/example-02-subNewPool/index.ts @@ -0,0 +1,20 @@ +import { + Connection, + PublicKey +} from '@solana/web3.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); +const raydiumV4PublicKey = new PublicKey('675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'); + +connection.onLogs( + raydiumV4PublicKey, + ({ logs, err, signature }) => { + if (err) return; + + if (logs && logs.some(log => log.includes("initialize2"))) { + console.log(`New liquidity pool created: https://solscan.io/tx/${signature}`); + } + }, + "confirmed" +); \ No newline at end of file diff --git a/en/example-03-subPrice/README.md b/en/example-03-subPrice/README.md new file mode 100644 index 0000000..5c2d00b --- /dev/null +++ b/en/example-03-subPrice/README.md @@ -0,0 +1,30 @@ +# Listening to Real-Time Prices of Raydium CLMM Tokens + +This example combines `onSlotUpdate` and buffer parsing methods to subscribe in real-time to the price of WSOL in the Raydium CLMM WSOL/USDC liquidity pool. + +Run it using `npx esrun example-03-subPrice/index.ts` + +``` +sqrtPriceX64Value at offset 253: 8617564287599812924 +WSOL Price: 218.23762107593976 +--- + +sqrtPriceX64Value at offset 253: 8618158284616202734 +WSOL Price: 218.2677077591724 +--- + +sqrtPriceX64Value at offset 253: 8617802344396074973 +WSOL Price: 218.24967869796686 +--- + +sqrtPriceX64Value at offset 253: 8616982572722284816 +WSOL Price: 218.2081585081117 +--- + +sqrtPriceX64Value at offset 253: 8617078429812061202 +WSOL Price: 218.21301332015403 +--- + +sqrtPriceX64Value at offset 253: 8616930983791568854 +WSOL Price: 218.2055457392501 +--- \ No newline at end of file diff --git a/en/example-03-subPrice/index.ts b/en/example-03-subPrice/index.ts new file mode 100644 index 0000000..af5ca09 --- /dev/null +++ b/en/example-03-subPrice/index.ts @@ -0,0 +1,31 @@ +import { + Connection, + PublicKey +} from '@solana/web3.js'; +import BN from 'bn.js'; + +const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); +// const connection = new Connection("https://mainnet-ams.chainbuff.com", "confirmed"); + +connection.onAccountChange( + new PublicKey('8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj'), + async (accountInfo) => { + const dataBuffer = accountInfo?.data; + if (!dataBuffer) { + throw new Error("Account data not found"); + } + + const offset = 253 + const sqrtPriceX64Buffer = dataBuffer.slice(offset, offset + 16); // Read 16 bytes + const sqrtPriceX64Value = new BN(sqrtPriceX64Buffer, 'le'); // Create BN instance using little-endian byte order + console.log(`sqrtPriceX64Value at offset ${offset}:`, sqrtPriceX64Value.toString()); + + // Calculate price + const sqrtPriceX64BigInt = BigInt(sqrtPriceX64Value.toString()); + const sqrtPriceX64Float = Number(sqrtPriceX64BigInt) / (2 ** 64); + const price = sqrtPriceX64Float ** 2 * 1e9 / 1e6; + console.log(`WSOL price:`, price.toString()) + console.log('---\n') + }, + 'confirmed' +); \ No newline at end of file