├── .gitignore ├── README.md ├── examples ├── .gitignore ├── bulk-nft-transfer.ts ├── bulk-sol-transfer.ts ├── bulk-spl-transfer.ts ├── get-sol-domains-for-address.ts ├── lite-example.ts ├── package-lock.json ├── package.json ├── rpc-summary.ts └── sol-transfer.ts ├── package-lock.json └── package ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── interfaces │ ├── ILogger.ts │ ├── ITransfer.ts │ └── IWallet.ts └── modules │ ├── ConnectionManager.ts │ ├── Disperse.ts │ ├── Logger.ts │ ├── SNSDomainResolver.ts │ ├── SingleTransactionWrapper.ts │ ├── TransactionBuilder.ts │ ├── TransactionHelper.ts │ ├── TransactionWrapper.ts │ └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

SolToolkit

3 |

4 | SDK npm package 5 | Docs 6 | Discord Chat 7 |

8 |
9 | 10 | # SolToolkit 11 | This repository provides open source access to SolToolkit (Typescript) SDK. 12 | 13 | ## Installation 14 | ``` 15 | npm i @solworks/soltoolkit-sdk 16 | ``` 17 | 18 | ## Modules 19 | 20 | ### ConnectionManager 21 | ConnectionManager is a singleton class that manages web3.js Connection(s). It takes the following parameters on initialization using the async `getInstance()` method: 22 | ```typescript 23 | { 24 | network: Cluster; 25 | endpoint?: string; 26 | endpoints?: string[]; 27 | config?: ConnectionConfig; 28 | commitment?: Commitment; 29 | mode?: Mode; 30 | } 31 | ``` 32 | #### Parameters 33 | - `network` is the cluster to connect to, possible values are 'mainnet-beta', 'testnet', 'devnet', 'localnet'. This is required. If you do not pass in any values for `endpoint` or `endpoints`, the default endpoints for the network will be used. 34 | - `endpoint` is a single endpoint to connect to. This is optional. 35 | - `endpoints` is an array of endpoints to connect to. This is optional. 36 | - `config` is a web3.js ConnectionConfig object. This is optional. 37 | - `commitment` is the commitment level to use for transactions. This is optional, will default to 'max'. 38 | - `mode` is the Mode for the ConnectionManager. This is optional, will default to 'single'. Possible values are: 39 | - 'single' - Uses the `endpoint` param, that falls back to the first endpoint provided in `endpoints`, that falls back to the default endpoints for the network. 40 | - 'first' - Uses the first endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 41 | - 'last' - Uses the last endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 42 | - 'round-robin' - Uses the endpoints provided in `endpoints` in a round-robin fashion (cycles through each endpoint in sequence starting from the first). Throws an error if no endpoints are provided. 43 | - 'random' - Uses a random endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 44 | - 'fastest' - Uses the fastest endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 45 | - 'highest-slot' - Uses the endpoint with the highest slot provided in `endpoints`. Throws an error if no endpoints are provided. 46 | 47 | #### Methods 48 | - `getInstance()` - Returns the singleton instance of the ConnectionManager. This method is async and must be awaited. 49 | - `getInstanceSync()` - Returns the singleton instance of the ConnectionManager. This method is synchronous. This method should only be used after initializing the ConnectionManager with `getInstance()`. 50 | - `conn()` - Returns a web3.js connection. This method will update the summary for each RPC to determine the 'fastest' or 'highest slot' endpoint. This method is async and must be awaited. 51 | - `connSync()` - Returns a web3.js connection. This method will use fastest' or 'highest slot' endpoint determined during initialization. This method is synchronous. 52 | 53 | ## Examples 54 | ### Fetching the fastest RPC endpoint 55 | ```typescript 56 | import { ConnectionManager } from "@solworks/soltoolkit-sdk"; 57 | 58 | (async () => { 59 | // create connection manager 60 | const cm = await ConnectionManager.getInstance({ 61 | commitment: "max", 62 | endpoints: [ 63 | "https://api.devnet.solana.com", 64 | "https://solana-devnet-rpc.allthatnode.com", 65 | "https://mango.devnet.rpcpool.com", 66 | "https://rpc.ankr.com/solana_devnet", 67 | ], 68 | mode: "fastest", 69 | network: "devnet" 70 | }); 71 | 72 | // get fastest endpoint 73 | const fastestEndpoint = cm._fastestEndpoint; 74 | console.log(`Fastest endpoint: ${fastestEndpoint}`); 75 | })(); 76 | ``` 77 | 78 | ### Fetching the highest slot RPC endpoint 79 | ```typescript 80 | import { ConnectionManager, Logger } from "@solworks/soltoolkit-sdk"; 81 | 82 | (async () => { 83 | // create connection manager 84 | const cm = await ConnectionManager.getInstance({ 85 | commitment: "max", 86 | endpoints: [ 87 | "https://api.devnet.solana.com", 88 | "https://solana-devnet-rpc.allthatnode.com", 89 | "https://mango.devnet.rpcpool.com", 90 | "https://rpc.ankr.com/solana_devnet", 91 | ], 92 | mode: "highest-slot", 93 | network: "devnet" 94 | }); 95 | 96 | // get highest slot endpoint 97 | const highestSlotEndpoint = cm._highestSlotEndpoint; 98 | console.log(`Highest slot endpoint: ${_highestSlotEndpoint}`); 99 | })(); 100 | ``` 101 | 102 | ### Fetching a summary of RPC speeds 103 | ```typescript 104 | import { ConnectionManager, Logger } from "@solworks/soltoolkit-sdk"; 105 | 106 | (async () => { 107 | const logger = new Logger("example"); 108 | 109 | // create connection manager 110 | const cm = await ConnectionManager.getInstance({ 111 | commitment: "max", 112 | endpoints: [ 113 | "https://api.devnet.solana.com", 114 | "https://solana-devnet-rpc.allthatnode.com", 115 | "https://mango.devnet.rpcpool.com", 116 | "https://rpc.ankr.com/solana_devnet", 117 | ], 118 | mode: "fastest", 119 | network: "devnet" 120 | }); 121 | 122 | // get summary of endpoint speeds 123 | const summary = await cm.getEndpointsSummary(); 124 | logger.debug(JSON.stringify(summary, null, 2)); 125 | })(); 126 | ``` 127 | 128 | ### Transfer SOL to 1 user 129 | ```typescript 130 | import { Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 131 | import { 132 | ConnectionManager, 133 | TransactionBuilder, 134 | TransactionWrapper, 135 | Logger 136 | } from "@solworks/soltoolkit-sdk"; 137 | 138 | const logger = new Logger("example"); 139 | const sender = Keypair.generate(); 140 | const receiver = Keypair.generate(); 141 | 142 | (async () => { 143 | // create connection manager 144 | const cm = await ConnectionManager.getInstance({ 145 | commitment: COMMITMENT, 146 | endpoints: [ 147 | "https://api.devnet.solana.com", 148 | "https://solana-devnet-rpc.allthatnode.com", 149 | "https://mango.devnet.rpcpool.com", 150 | "https://rpc.ankr.com/solana_devnet", 151 | ], 152 | mode: "fastest", 153 | network: "devnet", 154 | }); 155 | 156 | // airdrop sol to the generated address 157 | const airdropSig = await cm 158 | .connSync({ airdrop: true }) 159 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 160 | 161 | // confirm airdrop tx 162 | await TransactionWrapper.confirmTx({ 163 | connectionManager: cm, 164 | changeConn: false, 165 | signature: airdropSig, 166 | commitment: "max", 167 | }); 168 | 169 | // create builder and add token transfer ix 170 | var builder = TransactionBuilder 171 | .create() 172 | .addSolTransferIx({ 173 | from: sender.publicKey, 174 | to: receiver.publicKey, 175 | amountLamports: 10_000_000, 176 | }) 177 | .addMemoIx({ 178 | memo: "gm", 179 | signer: sender.publicKey, 180 | }); 181 | 182 | // build the transaction 183 | // returns a transaction with no fee payer or blockhash 184 | let tx = builder.build(); 185 | 186 | // feed transaction into TransactionWrapper 187 | const wrapper = await TransactionWrapper.create({ 188 | connectionManager: cm, 189 | transaction: tx, 190 | signer: sender.publicKey, 191 | }).addBlockhashAndFeePayer(); 192 | 193 | // sign the transaction 194 | const signedTx = await wrapper.sign({ 195 | signer: sender as Signer, 196 | }); 197 | 198 | // send and confirm the transaction 199 | const transferSig = await wrapper.sendAndConfirm({ 200 | serialisedTx: signedTx.serialize(), 201 | }); 202 | })(); 203 | ``` 204 | 205 | ### Send a memo to 1 user 206 | ```typescript 207 | import { Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 208 | import { 209 | ConnectionManager, 210 | TransactionBuilder, 211 | TransactionWrapper, 212 | Logger 213 | } from "@solworks/soltoolkit-sdk"; 214 | 215 | const logger = new Logger("example"); 216 | const sender = Keypair.generate(); 217 | const receiver = Keypair.generate(); 218 | 219 | (async () => { 220 | // create connection manager 221 | const cm = await ConnectionManager.getInstance({ 222 | commitment: COMMITMENT, 223 | endpoints: [ 224 | "https://api.devnet.solana.com", 225 | "https://solana-devnet-rpc.allthatnode.com", 226 | "https://mango.devnet.rpcpool.com", 227 | "https://rpc.ankr.com/solana_devnet", 228 | ], 229 | mode: "fastest", 230 | network: "devnet", 231 | }); 232 | 233 | // airdrop sol to the generated address 234 | const airdropSig = await cm 235 | .connSync({ airdrop: true }) 236 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 237 | 238 | // confirm airdrop tx 239 | await TransactionWrapper.confirmTx({ 240 | connectionManager: cm, 241 | changeConn: false, 242 | signature: airdropSig, 243 | commitment: "max", 244 | }); 245 | 246 | // create builder and add token transfer ix 247 | var builder = TransactionBuilder 248 | .create() 249 | .addMemoIx({ 250 | memo: "gm", 251 | signer: sender.publicKey, 252 | }); 253 | 254 | // build the transaction 255 | // returns a transaction with no fee payer or blockhash 256 | let tx = builder.build(); 257 | 258 | // feed transaction into TransactionWrapper 259 | const wrapper = await TransactionWrapper.create({ 260 | connectionManager: cm, 261 | transaction: tx, 262 | signer: sender.publicKey, 263 | }).addBlockhashAndFeePayer(); 264 | 265 | // sign the transaction 266 | const signedTx = await wrapper.sign({ 267 | signer: sender as Signer, 268 | }); 269 | 270 | // send and confirm the transaction 271 | const transferSig = await wrapper.sendAndConfirm({ 272 | serialisedTx: signedTx.serialize(), 273 | }); 274 | ``` 275 | 276 | ### Lookup address for .sol domains 277 | ```typescript 278 | import { PublicKey } from "@solana/web3.js"; 279 | import { 280 | SNSDomainResolver, 281 | Logger 282 | } from "@solworks/soltoolkit-sdk"; 283 | 284 | const logger = new Logger("example"); 285 | const addressString = '5F6gcdzpw7wUjNEugdsD4aLJdEQ4Wt8d6E85vaQXZQSJ'; 286 | const addressPublicKey = new PublicKey(addressString); 287 | 288 | (async () => { 289 | // use the SNSDomainResolver to get the first .sol domain associated with an address 290 | // getDomainFromAddress takes a PublicKey or an address string as an argument 291 | // getDomainFromAddress returns a Promise 292 | const firstDomainPk = await SNSDomainResolver.getDomainFromAddress(addressPublicKey); 293 | logger.info(`First domain for address: ${firstDomainPk || "no domain found"}`); 294 | const firstDomainString = await SNSDomainResolver.getDomainFromAddress(addressString); 295 | logger.info(`First domain for address: ${firstDomainString || "no domain found"}`); 296 | 297 | // use the SNSDomainResolver to get all .sol domains associated with an address 298 | // getDomainsFromAddress takes a PublicKey or an address string as an argument 299 | // getDomainsFromAddress returns a Promise 300 | const allDomainsPk = await SNSDomainResolver.getDomainsFromAddress(addressPublicKey); 301 | logger.info(`All domains for address: ${allDomainsPk || "no domains found"}`); 302 | const allDomainsString = await SNSDomainResolver.getDomainsFromAddress(addressString); 303 | logger.info(`All domains for address: ${allDomainsString || "no domains found"}`); 304 | })(); 305 | ``` 306 | 307 | ### Send a transaction using Jito 308 | ```typescript 309 | import { Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 310 | import { 311 | ConnectionManager, 312 | TransactionBuilder, 313 | TransactionWrapper, 314 | Logger, 315 | sendTxUsingJito 316 | } from "@solworks/soltoolkit-sdk"; 317 | 318 | (async () => { 319 | // create connection manager 320 | const cm = await ConnectionManager.getInstance({ 321 | commitment: 'processed', 322 | endpoints: [ 323 | "https://api.mainnet-beta.solana.com", 324 | ], 325 | mode: "fastest", 326 | network: "mainnet-beta", 327 | }); 328 | 329 | // create builder and add token transfer ix 330 | var builder = TransactionBuilder 331 | .create() 332 | .addMemoIx({ 333 | memo: "gm", 334 | signer: sender.publicKey, 335 | }); 336 | 337 | // build the transaction 338 | // returns a transaction with no fee payer or blockhash 339 | let tx = builder.build(); 340 | 341 | // feed transaction into TransactionWrapper 342 | const wrapper = await TransactionWrapper.create({ 343 | connectionManager: cm, 344 | transaction: tx, 345 | signer: sender.publicKey, 346 | }).addBlockhashAndFeePayer(); 347 | 348 | // sign the transaction 349 | const signedTx = await wrapper.sign({ 350 | signer: sender as Signer, 351 | }); 352 | 353 | // send and confirm the transaction 354 | const transferSig = await wrapper.sendTxUsingJito({ 355 | serialisedTx: signedTx.serialize(), 356 | region: 'mainnet', 357 | sendOptions: {} 358 | }); 359 | 360 | // OR use static method 361 | const transferSig = await sendTxUsingJito({ 362 | serialisedTx: signedTx.serialize(), 363 | region: 'mainnet', 364 | sendOptions: {} 365 | }); 366 | })(); 367 | ``` 368 | 369 | ### Dispersing SOL to 10,000 users in <120 seconds 370 | See [example](https://github.com/SolWorks-Dev/soltoolkit-sdk/blob/master/examples/bulk-sol-transfer.ts). 371 | 372 | 373 | ## License 374 | SolToolkit is licensed under [Affero GPL](https://www.gnu.org/licenses/agpl-3.0.txt). -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env -------------------------------------------------------------------------------- /examples/bulk-nft-transfer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | Keypair, 4 | LAMPORTS_PER_SOL, 5 | PublicKey, 6 | Signer, 7 | } from "@solana/web3.js"; 8 | import { 9 | ConnectionManager, 10 | TransactionWrapper, 11 | Logger, 12 | TransactionBuilder, 13 | } from "@solworks/soltoolkit-sdk"; 14 | import { 15 | getAssociatedTokenAddress, 16 | createTransferCheckedInstruction, 17 | createAssociatedTokenAccountInstruction 18 | } from "@solana/spl-token"; 19 | import { Metaplex } from "@metaplex-foundation/js"; 20 | import * as fs from "fs"; 21 | 22 | // This script will: 23 | // 1. Iterate through a list of mint addresses 24 | // 2. Create an associated token account for each mint address 25 | // 3. Transfer 1 NFT to each associated token account 26 | // 4. Confirm the transaction 27 | // 5. Log the transaction hash and result along with any errors 28 | const rpcEndpoint = 'https://api.mainnet-beta.solana.com'; 29 | const commitment: Commitment = "max"; 30 | const skipSending = false; 31 | const sender = Keypair.fromSecretKey(Uint8Array.from([...])); 32 | const minters = [{ 33 | "address": "...", 34 | "items": 3 35 | }, { 36 | "address": "...", 37 | "items": 3 38 | }, { 39 | "address": "...", 40 | "items": 12 41 | }]; 42 | 43 | (async () => { 44 | const logger = new Logger("nft-transfer"); 45 | const cm = await ConnectionManager.getInstance({ 46 | commitment, 47 | endpoint: rpcEndpoint, 48 | mode: "single", 49 | network: "mainnet-beta", 50 | }); 51 | const mp = new Metaplex(cm._connection); 52 | 53 | // get SOL balance of sender 54 | logger.debug("Fetching SOL balance of", sender.publicKey.toBase58()); 55 | let senderSOLBal = await cm 56 | .connSync({ changeConn: false }) 57 | .getBalance(sender.publicKey, commitment); 58 | logger.debug(`Sender balance: ${senderSOLBal / LAMPORTS_PER_SOL} SOL`); 59 | 60 | 61 | let results: IResults = { 62 | success: [], 63 | failure: [], 64 | }; 65 | // iterate through mints 66 | for (let i = 0; i < minters.length; i++) { 67 | // get NFTs owned by sender 68 | const nftsOwnedBySender = await mp 69 | .nfts() 70 | .findAllByOwner({ owner: sender.publicKey }); 71 | logger.debug("NFTs owned by sender:", nftsOwnedBySender.length); 72 | const receivingOwner = new PublicKey(minters[i].address); 73 | const nftsToSend = minters[i].items; 74 | 75 | // find minted nfts to send 76 | for (let k = 0; k < nftsToSend; k++) { 77 | if (nftsOwnedBySender.length === 0) { 78 | logger.debug("No more NFTs to send"); 79 | break; 80 | } 81 | 82 | const nftToSend = nftsOwnedBySender[k]; 83 | logger.debug("NFT to send:", nftToSend); 84 | const sendingMint = (nftToSend as any).mintAddress; 85 | logger.debug("Sending mint:", sendingMint.toBase58()); 86 | 87 | try { 88 | let sendingAta = await getAssociatedTokenAddress(sendingMint, sender.publicKey); 89 | logger.debug("Sending ATA:", sendingAta.toBase58()); 90 | 91 | let receivingAta = await getAssociatedTokenAddress(sendingMint, receivingOwner); 92 | logger.debug("Receiving ATA:", receivingAta.toBase58()); 93 | 94 | // generate tx to transfer NFT to ATA 95 | // create associated token account 96 | const tx = TransactionBuilder 97 | .create() 98 | .addIx([ 99 | createAssociatedTokenAccountInstruction( 100 | sender.publicKey, 101 | receivingAta, 102 | receivingOwner, 103 | sendingMint 104 | ), 105 | createTransferCheckedInstruction( 106 | sendingAta, 107 | sendingMint, 108 | receivingAta, 109 | sender.publicKey, 110 | 1, 111 | 0 112 | ) 113 | ]) 114 | .build(); 115 | 116 | 117 | if (!skipSending) { 118 | // feed transaction into TransactionWrapper 119 | const wrapper = await TransactionWrapper 120 | .create({ 121 | connectionManager: cm, 122 | transaction: tx, 123 | signer: sender.publicKey, 124 | }) 125 | .addBlockhashAndFeePayer(sender.publicKey); 126 | 127 | // sign the transaction 128 | logger.debug(`Signing transaction ${i + 1}`); 129 | const signedTx = await wrapper.sign({ signer: sender as Signer }); 130 | 131 | // send and confirm the transaction 132 | logger.debug(`Sending transaction ${i + 1}`); 133 | const transferSig = await wrapper.sendAndConfirm({ 134 | serialisedTx: signedTx.serialize(), 135 | commitment 136 | }); 137 | logger.debug("Transaction sent:", transferSig.toString()); 138 | 139 | results.success.push({ 140 | sentTicketMint: sendingMint.toBase58(), 141 | ticketHeldMint: receivingOwner, 142 | }); 143 | 144 | await sleep(3_000); 145 | } 146 | } catch (e: any) { 147 | logger.error(e); 148 | results.failure.push({ 149 | sentTicketMint: sendingMint.toBase58(), 150 | ticketHeldMint: receivingOwner.toBase58() 151 | }); 152 | } 153 | } 154 | } 155 | 156 | 157 | fs.writeFileSync("results.json", JSON.stringify(results)); 158 | })(); 159 | 160 | interface IResults { 161 | success: Array; 162 | failure: Array; 163 | } 164 | function sleep(ms: number) { 165 | return new Promise((resolve) => setTimeout(resolve, ms)); 166 | } -------------------------------------------------------------------------------- /examples/bulk-sol-transfer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | Keypair, 4 | LAMPORTS_PER_SOL, 5 | Signer, 6 | Transaction, 7 | } from "@solana/web3.js"; 8 | import { 9 | ConnectionManager, 10 | Disperse, 11 | TransactionBuilder, 12 | TransactionWrapper, 13 | Logger 14 | } from "@solworks/soltoolkit-sdk"; 15 | 16 | const COMMITMENT: Commitment = "confirmed"; 17 | const NO_OF_RECEIVERS = 10_000; 18 | const CHUNK_SIZE = 30; 19 | const TOTAL_SOL = 10; 20 | 21 | const SKIP_AIRDROP = true; 22 | const SKIP_SENDING = false; 23 | const SKIP_BALANCE_CHECK = true; 24 | 25 | // generate keypair for example 26 | const sender = Keypair.fromSecretKey( 27 | Uint8Array.from([ 28 | 36, 50, 153, 146, 147, 239, 210, 72, 199, 68, 75, 220, 42, 139, 105, 61, 29 | 148, 117, 55, 75, 23, 144, 30, 206, 138, 255, 51, 206, 102, 239, 73, 28, 30 | 240, 73, 69, 190, 238, 27, 112, 36, 151, 255, 182, 64, 13, 173, 94, 115, 31 | 111, 45, 2, 154, 250, 93, 100, 44, 251, 111, 229, 34, 193, 249, 199, 238, 32 | ]) 33 | ); 34 | 35 | (async () => { 36 | const logger = new Logger("example"); 37 | 38 | // create connection manager 39 | const cm = await ConnectionManager.getInstance({ 40 | commitment: COMMITMENT, 41 | endpoints: [ 42 | "https://mango.devnet.rpcpool.com", 43 | 44 | // https://docs.solana.com/cluster/rpc-endpoints 45 | // Maximum number of requests per 10 seconds per IP: 100 (10/s) 46 | // Maximum number of requests per 10 seconds per IP for a single RPC: 40 (4/s) 47 | // Maximum concurrent connections per IP: 40 48 | // Maximum connection rate per 10 seconds per IP: 40 49 | // Maximum amount of data per 30 second: 100 MB 50 | // "https://api.devnet.solana.com", 51 | 52 | // https://shdw.genesysgo.com/genesysgo/the-genesysgo-rpc-network 53 | // SendTransaction Limit: 10 RPS + 200 Burst 54 | // getProgramAccounts Limit: 15 RPS + 5 burst 55 | // Global Limit on the rest of the calls: 200 RPS 56 | "https://devnet.genesysgo.net", 57 | ], 58 | mode: "round-robin", 59 | network: "devnet", 60 | }); 61 | 62 | if (!SKIP_AIRDROP) { 63 | // airdrop 1 sol to new addresses, confirm and send sol to SENDER 64 | for (let i = 0; i < Math.ceil((TOTAL_SOL + 1) / 1); i++) { 65 | // generate new keypair 66 | const keypair = Keypair.generate(); 67 | 68 | // airdrop sol to the generated address 69 | const airdropSig = await cm 70 | .connSync({ airdrop: true }) 71 | .requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL); 72 | logger.debug("Airdropped 1 SOL to", sender.publicKey.toBase58()); 73 | 74 | // wait for confirmation 75 | logger.debug("Confirming airdrop transaction..."); 76 | await TransactionWrapper.confirmTx({ 77 | connectionManager: cm, 78 | changeConn: false, 79 | signature: airdropSig, 80 | commitment: "max", 81 | airdrop: true, 82 | }); 83 | logger.debug("Airdrop transaction confirmed"); 84 | 85 | // send sol to SENDER 86 | const tx = TransactionBuilder.create() 87 | .addSolTransferIx({ 88 | from: keypair.publicKey, 89 | to: sender.publicKey, 90 | amountLamports: LAMPORTS_PER_SOL - 5000, 91 | }) 92 | .build(); 93 | 94 | const wrapper = await TransactionWrapper.create({ 95 | connectionManager: cm, 96 | changeConn: false, 97 | signer: keypair.publicKey, 98 | transaction: tx, 99 | }).addBlockhashAndFeePayer(keypair.publicKey); 100 | const signedTx = await wrapper.sign({ signer: keypair as Signer }); 101 | const sig = await wrapper.sendAndConfirm({ 102 | serialisedTx: signedTx.serialize(), 103 | commitment: "max", 104 | }); 105 | logger.debug( 106 | "Sent 1 SOL to", 107 | sender.publicKey.toBase58(), 108 | "with signature", 109 | sig 110 | ); 111 | 112 | await sleep(1000); 113 | } 114 | } 115 | 116 | // fetch balance of the generated address 117 | logger.debug("Fetching balance of", sender.publicKey.toBase58()); 118 | let senderBal = await cm 119 | // default value for changeConn = true 120 | .connSync({ changeConn: true }) 121 | .getBalance(sender.publicKey, COMMITMENT); 122 | logger.debug(`Sender balance: ${senderBal}`); 123 | 124 | // generate receivers 125 | logger.debug(`Generating ${NO_OF_RECEIVERS} receivers...`); 126 | const receivers: Keypair[] = []; 127 | for (let i = 0; i < NO_OF_RECEIVERS; i++) { 128 | receivers.push(Keypair.generate()); 129 | } 130 | logger.debug("Receivers generated"); 131 | 132 | // generate transactions 133 | const transfers: { 134 | amount: number; 135 | recipient: string; 136 | }[] = []; 137 | 138 | const rentCost = (NO_OF_RECEIVERS+1) * 5_000; 139 | const transferAmount = Math.floor( 140 | (senderBal - rentCost) / NO_OF_RECEIVERS 141 | ); 142 | logger.debug(`Sending ${transferAmount} to ${NO_OF_RECEIVERS} receivers`); 143 | for (let i = 0; i < NO_OF_RECEIVERS; i++) { 144 | transfers.push({ 145 | amount: transferAmount, 146 | recipient: receivers[i].publicKey.toBase58(), 147 | }); 148 | } 149 | 150 | // send transactions 151 | if (!SKIP_SENDING) { 152 | const transactions = await Disperse.create({ 153 | tokenType: "SOL", 154 | sender: sender.publicKey, 155 | transfers, 156 | }).generateTransactions(); 157 | 158 | const txChunks = chunk(transactions, CHUNK_SIZE); 159 | for (let i = 0; i < txChunks.length; i++) { 160 | logger.debug(`Sending transactions ${i + 1}/${txChunks.length}`); 161 | const txChunk = txChunks[i]; 162 | const conn = cm.connSync({ changeConn: true }); 163 | 164 | await Promise.all( 165 | txChunk.map(async (tx: Transaction, i: number) => { 166 | logger.debug(`Sending transaction ${i + 1}`); 167 | 168 | // feed transaction into TransactionWrapper 169 | const wrapper = await TransactionWrapper.create({ 170 | connection: conn, 171 | transaction: tx, 172 | signer: sender.publicKey, 173 | }).addBlockhashAndFeePayer(sender.publicKey); 174 | 175 | // sign the transaction 176 | logger.debug(`Signing transaction ${i + 1}`); 177 | const signedTx = await wrapper.sign({ 178 | signer: sender as Signer, 179 | }); 180 | 181 | // send and confirm the transaction 182 | logger.debug(`Sending transaction ${i + 1}`); 183 | const transferSig = await wrapper.sendAndConfirm({ 184 | serialisedTx: signedTx.serialize(), 185 | commitment: COMMITMENT, 186 | }); 187 | logger.debug("Transaction sent:", transferSig.toString()); 188 | }) 189 | ); 190 | await sleep(1_000); 191 | } 192 | } 193 | 194 | if (!SKIP_BALANCE_CHECK) { 195 | // fetch balance of the generated address 196 | logger.debug("Fetching balance of:", sender.publicKey.toBase58()); 197 | senderBal = await cm 198 | .connSync({ changeConn: true }) 199 | .getBalance(sender.publicKey, COMMITMENT); 200 | logger.debug(`Sender balance: ${senderBal}`); 201 | 202 | // split addresses into chunks of CHUNK_SIZE 203 | const chunks = chunk(receivers, CHUNK_SIZE); 204 | const balances: { 205 | balance: number; 206 | address: string; 207 | }[] = []; 208 | for (let i = 0; i < chunks.length; i++) { 209 | const chunk = chunks[i]; 210 | logger.debug( 211 | `Fetching balances for chunk ${i + 1} with ${chunk.length} addresses` 212 | ); 213 | 214 | // cycle to new connection to avoid rate limiting 215 | let conn = cm.connSync({ changeConn: true }); 216 | 217 | // fetch balances 218 | const results = await Promise.all( 219 | chunk.map(async (receiver: Keypair) => { 220 | const balance = await conn.getBalance(receiver.publicKey, COMMITMENT); 221 | logger.debug( 222 | `Balance of ${receiver.publicKey.toBase58()}: ${balance}` 223 | ); 224 | return { 225 | balance, 226 | address: receiver.publicKey.toBase58(), 227 | }; 228 | }) 229 | ); 230 | 231 | // add results to balances 232 | balances.push(...results); 233 | await sleep(1_000); 234 | } 235 | 236 | const totalBalance = balances.reduce((acc, curr) => acc + curr.balance, 0); 237 | const numberWithNoBalance = balances.filter((b) => b.balance === 0).length; 238 | const numberWithBalance = balances.filter((b) => b.balance > 0).length; 239 | logger.debug(`Total amount sent: ${totalBalance}`); 240 | logger.debug(`Number of addresses with no balance: ${numberWithNoBalance}`); 241 | logger.debug(`Number of addresses with balance: ${numberWithBalance}`); 242 | } 243 | })(); 244 | 245 | function chunk(arr: any[], len: number) { 246 | var chunks: any[] = [], 247 | i = 0, 248 | n = arr.length; 249 | 250 | while (i < n) { 251 | chunks.push(arr.slice(i, (i += len))); 252 | } 253 | return chunks; 254 | } 255 | 256 | function sleep(ms: number) { 257 | return new Promise((resolve) => setTimeout(resolve, ms)); 258 | } 259 | -------------------------------------------------------------------------------- /examples/bulk-spl-transfer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Commitment, 3 | Keypair, 4 | LAMPORTS_PER_SOL, 5 | Signer, 6 | Transaction, 7 | TransactionInstruction, 8 | } from "@solana/web3.js"; 9 | import { 10 | ConnectionManager, 11 | TransactionWrapper, 12 | Logger, 13 | TransactionBuilder, 14 | } from "@solworks/soltoolkit-sdk"; 15 | import { 16 | createAssociatedTokenAccountInstruction, 17 | createMint, 18 | mintToChecked, 19 | createAssociatedTokenAccount, 20 | getAssociatedTokenAddress, 21 | } from "@solana/spl-token"; 22 | 23 | const COMMITMENT: Commitment = "processed"; // "processed" is the fastest, "max" is ideal but takes longer 24 | const NO_OF_RECEIVERS = 10_000; // number of users to airdrop to 25 | const CHUNK_SIZE = 15; // transactions sent at once 26 | const DELAY_BETWEEN_CHUNKS_MS = 5_000; // x/100 seconds 27 | const SKIP_SENDING = false; // send transactions 28 | const SKIP_BALANCE_CHECK = true; // fetch balance after sending 29 | const TOKEN_DECIMALS = 8; // decimals for SPL token 30 | // max tokens to mint 31 | const MAX_TOKENS = 1_000_000 * 10 ** TOKEN_DECIMALS; 32 | 33 | // swap with your own keypair or load from file 34 | const sender = Keypair.fromSecretKey( 35 | Uint8Array.from([ 36 | 36, 50, 153, 146, 147, 239, 210, 72, 199, 68, 75, 220, 42, 139, 105, 61, 37 | 148, 117, 55, 75, 23, 144, 30, 206, 138, 255, 51, 206, 102, 239, 73, 28, 38 | 240, 73, 69, 190, 238, 27, 112, 36, 151, 255, 182, 64, 13, 173, 94, 115, 39 | 111, 45, 2, 154, 250, 93, 100, 44, 251, 111, 229, 34, 193, 249, 199, 238, 40 | ]) 41 | ); 42 | 43 | (async () => { 44 | const logger = new Logger("example"); 45 | const cm = await ConnectionManager.getInstance({ 46 | commitment: COMMITMENT, 47 | endpoint: "https://api.devnet.solana.com", 48 | mode: "single", 49 | network: "devnet", 50 | }); 51 | 52 | // airdrop sol to the generated address (devnet only) 53 | // this can error if the RPC doesn't have airdrop enabled 54 | let airdropSig = await cm 55 | .connSync({ airdrop: true }) 56 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 57 | logger.debug("Airdropped 1 SOL to", sender.publicKey.toBase58()); 58 | 59 | logger.debug("Confirming airdrop transaction..."); 60 | await TransactionWrapper.confirmTx({ 61 | connectionManager: cm, 62 | changeConn: false, 63 | signature: airdropSig, 64 | commitment: "max", 65 | airdrop: true, 66 | }); 67 | logger.debug("Airdrop transaction confirmed"); 68 | 69 | // get SOL balance of sender 70 | logger.debug("Fetching SOL balance of", sender.publicKey.toBase58()); 71 | let senderSOLBal = await cm 72 | .connSync({ changeConn: false }) 73 | .getBalance(sender.publicKey, COMMITMENT); 74 | logger.debug(`Sender balance: ${senderSOLBal / LAMPORTS_PER_SOL} SOL`); 75 | 76 | // create mint account and tx for initializing it 77 | let mint = await createMint( 78 | cm.connSync({ changeConn: false }), 79 | sender, 80 | sender.publicKey, 81 | sender.publicKey, 82 | TOKEN_DECIMALS 83 | ); 84 | logger.debug(`Mint created: ${mint.toBase58()}`); 85 | 86 | // create associated token account 87 | let associatedAddr = await createAssociatedTokenAccount( 88 | cm.connSync({ changeConn: false }), 89 | sender, 90 | mint, 91 | sender.publicKey 92 | ); 93 | logger.debug("ATA address:", associatedAddr.toBase58()); 94 | 95 | // mint tokens to the associated token account 96 | let mintTokensTx = await mintToChecked( 97 | cm.connSync({ changeConn: false }), 98 | sender, 99 | mint, 100 | associatedAddr, 101 | sender.publicKey, 102 | MAX_TOKENS, 103 | TOKEN_DECIMALS 104 | ); 105 | logger.debug(`Minted ${MAX_TOKENS} tokens to ${associatedAddr.toBase58()}`); 106 | logger.debug(`Mint tx: ${mintTokensTx}`); 107 | 108 | logger.debug("Fetching balance of", associatedAddr.toBase58()); 109 | let senderTokenBal = await cm 110 | .connSync({ changeConn: false }) 111 | .getTokenAccountBalance(associatedAddr, COMMITMENT); 112 | logger.debug(`Sender balance: ${senderTokenBal.value.uiAmount} tokens`); 113 | 114 | // generate receivers 115 | logger.debug(`Generating ${NO_OF_RECEIVERS} receivers...`); 116 | const receivers: Keypair[] = []; 117 | for (let i = 0; i < NO_OF_RECEIVERS; i++) { 118 | receivers.push(Keypair.generate()); 119 | } 120 | logger.debug("Receivers generated"); 121 | 122 | // generate transactions 123 | const missingAccountIxs: TransactionInstruction[] = []; 124 | const transactions: Transaction[] = []; 125 | 126 | logger.debug("Fetching balance of", associatedAddr.toBase58()); 127 | let senderBal = ( 128 | await cm 129 | .connSync({ changeConn: true }) 130 | .getTokenAccountBalance(associatedAddr, COMMITMENT) 131 | ).value.amount; 132 | logger.debug(`Sender balance: ${senderBal}`); 133 | const transferAmount = Math.floor(Number(senderBal) / NO_OF_RECEIVERS); 134 | 135 | logger.debug(`Sending ${transferAmount} to ${NO_OF_RECEIVERS} receivers`); 136 | for (let i = 0; i < NO_OF_RECEIVERS; i++) { 137 | const ata = await getAssociatedTokenAddress(mint, receivers[i].publicKey); 138 | const ix = createAssociatedTokenAccountInstruction( 139 | sender.publicKey, 140 | ata, 141 | receivers[i].publicKey, 142 | mint 143 | ); 144 | missingAccountIxs.push(ix); 145 | } 146 | 147 | // generate transactions for create mint accounts 148 | // split into chunks of 12 ixs 149 | const missingAccountIxsChunks = chunk(missingAccountIxs, 12); 150 | for (let i = 0; i < missingAccountIxsChunks.length; i++) { 151 | const chunk = missingAccountIxsChunks[i]; 152 | const tx = TransactionBuilder.create().addIx(chunk).build(); 153 | transactions.push(tx); 154 | } 155 | 156 | // send transactions 157 | if (!SKIP_SENDING) { 158 | const txChunks = chunk(transactions, CHUNK_SIZE); 159 | for (let i = 0; i < txChunks.length; i++) { 160 | logger.debug(`Sending transactions ${i + 1}/${txChunks.length}`); 161 | const txChunk = txChunks[i]; 162 | const conn = cm.connSync({ changeConn: true }); 163 | 164 | await Promise.all( 165 | txChunk.map(async (tx: Transaction, i: number) => { 166 | logger.debug(`Sending transaction ${i + 1}`); 167 | 168 | // feed transaction into TransactionWrapper 169 | const wrapper = await TransactionWrapper.create({ 170 | connection: conn, 171 | transaction: tx, 172 | signer: sender.publicKey, 173 | }).addBlockhashAndFeePayer(sender.publicKey); 174 | 175 | // sign the transaction 176 | logger.debug(`Signing transaction ${i + 1}`); 177 | const signedTx = (await wrapper.sign({ 178 | signers: [sender as Signer], 179 | }))[0]; 180 | 181 | // send and confirm the transaction 182 | logger.debug(`Sending transaction ${i + 1}`); 183 | const transferSig = await wrapper.sendAndConfirm({ 184 | serialisedTx: signedTx.serialize(), 185 | commitment: COMMITMENT, 186 | }); 187 | logger.debug("Transaction sent:", transferSig.toString()); 188 | }) 189 | ); 190 | await sleep(DELAY_BETWEEN_CHUNKS_MS); 191 | } 192 | } 193 | 194 | if (!SKIP_BALANCE_CHECK) { 195 | // fetch balance of the generated address 196 | logger.debug("Fetching balance of:", sender.publicKey.toBase58()); 197 | senderBal = ( 198 | await cm 199 | .connSync({ changeConn: true }) 200 | .getTokenAccountBalance(associatedAddr, COMMITMENT) 201 | ).value.amount; 202 | logger.debug(`Sender balance: ${senderBal}`); 203 | 204 | // split addresses into chunks of CHUNK_SIZE 205 | const chunks = chunk(receivers, CHUNK_SIZE); 206 | const balances: { 207 | balance: number; 208 | address: string; 209 | }[] = []; 210 | for (let i = 0; i < chunks.length; i++) { 211 | const chunk = chunks[i]; 212 | logger.debug( 213 | `Fetching balances for chunk ${i + 1} with ${chunk.length} addresses` 214 | ); 215 | 216 | // cycle to new connection to avoid rate limiting 217 | let conn = cm.connSync({ changeConn: true }); 218 | 219 | // fetch balances 220 | const results = await Promise.all( 221 | chunk.map(async (receiver: Keypair) => { 222 | const balance = await conn.getBalance(receiver.publicKey, COMMITMENT); 223 | logger.debug( 224 | `Balance of ${receiver.publicKey.toBase58()}: ${balance}` 225 | ); 226 | return { 227 | balance, 228 | address: receiver.publicKey.toBase58(), 229 | }; 230 | }) 231 | ); 232 | 233 | // add results to balances 234 | balances.push(...results); 235 | await sleep(DELAY_BETWEEN_CHUNKS_MS); 236 | } 237 | 238 | const totalBalance = balances.reduce((acc, curr) => acc + curr.balance, 0); 239 | const numberWithNoBalance = balances.filter((b) => b.balance === 0).length; 240 | const numberWithBalance = balances.filter((b) => b.balance > 0).length; 241 | logger.debug(`Total amount sent: ${totalBalance}`); 242 | logger.debug(`Number of addresses with no balance: ${numberWithNoBalance}`); 243 | logger.debug(`Number of addresses with balance: ${numberWithBalance}`); 244 | } 245 | })(); 246 | 247 | function chunk(arr: any[], len: number) { 248 | var chunks: any[] = [], 249 | i = 0, 250 | n = arr.length; 251 | 252 | while (i < n) { 253 | chunks.push(arr.slice(i, (i += len))); 254 | } 255 | return chunks; 256 | } 257 | 258 | export function sleep(ms: number) { 259 | return new Promise((resolve) => setTimeout(resolve, ms)); 260 | } 261 | -------------------------------------------------------------------------------- /examples/get-sol-domains-for-address.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { 3 | SNSDomainResolver, 4 | Logger 5 | } from "@solworks/soltoolkit-sdk"; 6 | 7 | const logger = new Logger("example"); 8 | const addressString = '5F6gcdzpw7wUjNEugdsD4aLJdEQ4Wt8d6E85vaQXZQSJ'; 9 | const addressPublicKey = new PublicKey(addressString); 10 | 11 | (async () => { 12 | // use the SNSDomainResolver to get the first .sol domain associated with an address 13 | // getDomainFromAddress takes a PublicKey or an address string as an argument 14 | // getDomainFromAddress returns a Promise 15 | const firstDomainPk = await SNSDomainResolver.getDomainFromAddress(addressPublicKey); 16 | logger.info(`First domain for address: ${firstDomainPk || "no domain found"}`); 17 | const firstDomainString = await SNSDomainResolver.getDomainFromAddress(addressString); 18 | logger.info(`First domain for address: ${firstDomainString || "no domain found"}`); 19 | 20 | // use the SNSDomainResolver to get all .sol domains associated with an address 21 | // getDomainsFromAddress takes a PublicKey or an address string as an argument 22 | // getDomainsFromAddress returns a Promise 23 | const allDomainsPk = await SNSDomainResolver.getDomainsFromAddress(addressPublicKey); 24 | logger.info(`All domains for address: ${allDomainsPk || "no domains found"}`); 25 | const allDomainsString = await SNSDomainResolver.getDomainsFromAddress(addressString); 26 | logger.info(`All domains for address: ${allDomainsString || "no domains found"}`); 27 | })(); -------------------------------------------------------------------------------- /examples/lite-example.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionManager } from "../package/build/index"; 2 | 3 | (async () => { 4 | // create connection manager 5 | const cm = await ConnectionManager.getInstance({ 6 | commitment: 'single', 7 | endpoints: [ 8 | "https://api.mainnet-beta.solana.com", 9 | "https://api.devnet.solana.com", 10 | "https://api.testnet.solana.com", 11 | ], 12 | mode: 'fastest', 13 | network: 'mainnet-beta', 14 | verbose: true 15 | }); 16 | })(); 17 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "license": "ISC", 11 | "dependencies": { 12 | "@metaplex-foundation/js": "^0.18.0", 13 | "@solana/spl-token": "^0.2.0", 14 | "@solana/web3.js": "^1.73.2", 15 | "@solworks/soltoolkit-sdk": "^0.0.27", 16 | "@types/node-fetch": "^2.6.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/rpc-summary.ts: -------------------------------------------------------------------------------- 1 | import { Commitment } from "@solana/web3.js"; 2 | import { Logger, ConnectionManager } from "../package/build/index"; 3 | 4 | (async () => { 5 | const logger = new Logger("example"); 6 | 7 | // create connection manager 8 | // only needs to be created once as it is a singleton 9 | const cm = await ConnectionManager.getInstance({ 10 | // commitment will be set to 'processed' if not provided 11 | commitment: 'single', 12 | 13 | // provide an array of endpoints to connect to or use `endpoint` to connect to a single endpoint 14 | endpoints: [ 15 | "https://api.mainnet-beta.solana.com", 16 | "https://api.devnet.solana.com", 17 | ], 18 | 19 | // mode will be set to 'latest' if not provided 20 | mode: 'latest-valid-block-height', 21 | 22 | // network must be provided, airdrop only supported on devnet 23 | network: 'mainnet-beta', 24 | 25 | // verbose will be set to false if not provided 26 | verbose: false 27 | }); 28 | 29 | // get fastest endpoint 30 | const fastest = cm._fastestEndpoint; 31 | logger.debug(`Fastest endpoint: ${fastest}`); 32 | 33 | // get highest slot endpoint 34 | const highestSlot = cm._highestSlotEndpoint; 35 | logger.debug(`Highest slot endpoint: ${highestSlot}`); 36 | 37 | // get latest block height endpoint 38 | const latestBlockHeight = cm._latestValidBlockHeightEndpoint; 39 | logger.debug(`Latest block height endpoint: ${latestBlockHeight}`); 40 | 41 | // get current connection endpoint 42 | const current = cm.connSync({ changeConn: false }).rpcEndpoint; 43 | logger.debug(`Current endpoint: ${current}`); 44 | })(); 45 | -------------------------------------------------------------------------------- /examples/sol-transfer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "../package/src/modules/Logger"; 2 | import { Commitment, Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 3 | import { 4 | ConnectionManager, 5 | TransactionBuilder, 6 | TransactionWrapper, 7 | getJitoEndpoint 8 | } from "../package/src/index"; 9 | 10 | const COMMITMENT: Commitment = "confirmed"; 11 | 12 | // generate keypair for example 13 | const sender = Keypair.generate(); 14 | const receiver = Keypair.generate(); 15 | 16 | (async () => { 17 | const logger = new Logger("example"); 18 | 19 | // create connection manager 20 | const cm = await ConnectionManager.getInstance({ 21 | commitment: COMMITMENT, 22 | endpoints: [ 23 | "https://api.devnet.solana.com", 24 | "https://solana-devnet-rpc.allthatnode.com", 25 | "https://mango.devnet.rpcpool.com", 26 | "https://rpc.ankr.com/solana_devnet", 27 | ], 28 | mode: "fastest", 29 | network: "devnet", 30 | }); 31 | 32 | // airdrop sol to the generated address 33 | logger.debug("Airdropping 1 SOL to:", sender.publicKey.toBase58()); 34 | const airdropSig = await cm 35 | .connSync({ airdrop: true }) 36 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 37 | 38 | // confirm airdrop tx 39 | logger.debug(`Confirming transaction ${airdropSig}`); 40 | await TransactionWrapper.confirmTx({ 41 | connectionManager: cm, 42 | changeConn: false, 43 | signature: airdropSig, 44 | commitment: "max", 45 | }); 46 | logger.debug("Airdrop transaction confirmed"); 47 | 48 | // fetch balance of the generated address 49 | logger.debug("Fetching balance of:", sender.publicKey.toBase58()); 50 | let senderBal = await cm.connSync({}).getBalance(sender.publicKey, COMMITMENT); 51 | logger.debug(`Sender balance: ${senderBal}`); 52 | 53 | logger.debug("Fetching balance of:", receiver.publicKey.toBase58()); 54 | let receiverBal = await cm 55 | .connSync({}) 56 | .getBalance(receiver.publicKey, COMMITMENT); 57 | logger.debug(`Receiver balance: ${receiverBal}`); 58 | 59 | // create builder and add token transfer ix 60 | logger.debug("Creating transaction"); 61 | var builder = TransactionBuilder 62 | .create() 63 | .addSolTransferIx({ 64 | from: sender.publicKey, 65 | to: receiver.publicKey, 66 | amountLamports: 10_000_000, 67 | }) 68 | .addMemoIx({ 69 | memo: "gm", 70 | signer: sender.publicKey, 71 | }) 72 | .addComputeBudgetIx({ 73 | units: 1_000_000, 74 | }); 75 | 76 | // build the transaction 77 | // returns a transaction with no fee payer or blockhash 78 | let tx = builder.build(); 79 | 80 | // feed transaction into TransactionWrapper 81 | const wrapper = await TransactionWrapper.create({ 82 | connectionManager: cm, 83 | transaction: tx, 84 | signer: sender.publicKey, 85 | }).addBlockhashAndFeePayer(); 86 | 87 | // sign the transaction 88 | const signedTxs = await wrapper.sign({ 89 | signers: [sender as Signer], 90 | }); 91 | 92 | // send and confirm the transaction 93 | const transferSig = await wrapper.sendAndConfirm({ 94 | serialisedTx: signedTx.serialize(), 95 | commitment: COMMITMENT, 96 | }); 97 | logger.debug("Transaction sent:", transferSig.toString()); 98 | 99 | // fetch balance of the generated address 100 | logger.debug("Fetching balance of:", sender.publicKey.toBase58()); 101 | senderBal = await cm.connSync({}).getBalance(sender.publicKey, COMMITMENT); 102 | logger.debug(`Sender balance: ${senderBal}`); 103 | 104 | logger.debug("Fetching balance of:", receiver.publicKey.toBase58()); 105 | receiverBal = await cm.connSync({}).getBalance(receiver.publicKey, COMMITMENT); 106 | logger.debug(`Receiver balance: ${receiverBal}`); 107 | })(); 108 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soltoolkit-sdk", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env -------------------------------------------------------------------------------- /package/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 4 7 | } -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 |
2 |

SolToolkit

3 |

4 | SDK npm package 5 | Docs 6 | Discord Chat 7 |

8 |
9 | 10 | # SolToolkit 11 | This repository provides open source access to SolToolkit (Typescript) SDK. 12 | 13 | ## Installation 14 | ``` 15 | npm i @solworks/soltoolkit-sdk 16 | ``` 17 | 18 | ## Modules 19 | 20 | ### ConnectionManager 21 | ConnectionManager is a singleton class that manages web3.js Connection(s). It takes the following parameters on initialization using the async `getInstance()` method: 22 | ```typescript 23 | { 24 | network: Cluster; 25 | endpoint?: string; 26 | endpoints?: string[]; 27 | config?: ConnectionConfig; 28 | commitment?: Commitment; 29 | mode?: Mode; 30 | } 31 | ``` 32 | #### Parameters 33 | - `network` is the cluster to connect to, possible values are 'mainnet-beta', 'testnet', 'devnet', 'localnet'. This is required. If you do not pass in any values for `endpoint` or `endpoints`, the default endpoints for the network will be used. 34 | - `endpoint` is a single endpoint to connect to. This is optional. 35 | - `endpoints` is an array of endpoints to connect to. This is optional. 36 | - `config` is a web3.js ConnectionConfig object. This is optional. 37 | - `commitment` is the commitment level to use for transactions. This is optional, will default to 'max'. 38 | - `mode` is the Mode for the ConnectionManager. This is optional, will default to 'single'. Possible values are: 39 | - 'single' - Uses the `endpoint` param, that falls back to the first endpoint provided in `endpoints`, that falls back to the default endpoints for the network. 40 | - 'first' - Uses the first endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 41 | - 'last' - Uses the last endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 42 | - 'round-robin' - Uses the endpoints provided in `endpoints` in a round-robin fashion (cycles through each endpoint in sequence starting from the first). Throws an error if no endpoints are provided. 43 | - 'random' - Uses a random endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 44 | - 'fastest' - Uses the fastest endpoint provided in `endpoints`. Throws an error if no endpoints are provided. 45 | - 'highest-slot' - Uses the endpoint with the highest slot provided in `endpoints`. Throws an error if no endpoints are provided. 46 | 47 | #### Methods 48 | - `getInstance()` - Returns the singleton instance of the ConnectionManager. This method is async and must be awaited. 49 | - `getInstanceSync()` - Returns the singleton instance of the ConnectionManager. This method is synchronous. This method should only be used after initializing the ConnectionManager with `getInstance()`. 50 | - `conn()` - Returns a web3.js connection. This method will update the summary for each RPC to determine the 'fastest' or 'highest slot' endpoint. This method is async and must be awaited. 51 | - `connSync()` - Returns a web3.js connection. This method will use fastest' or 'highest slot' endpoint determined during initialization. This method is synchronous. 52 | 53 | ## Examples 54 | ### Fetching the fastest RPC endpoint 55 | ```typescript 56 | import { ConnectionManager } from "@solworks/soltoolkit-sdk"; 57 | 58 | (async () => { 59 | // create connection manager 60 | const cm = await ConnectionManager.getInstance({ 61 | commitment: "max", 62 | endpoints: [ 63 | "https://api.devnet.solana.com", 64 | "https://solana-devnet-rpc.allthatnode.com", 65 | "https://mango.devnet.rpcpool.com", 66 | "https://rpc.ankr.com/solana_devnet", 67 | ], 68 | mode: "fastest", 69 | network: "devnet" 70 | }); 71 | 72 | // get fastest endpoint 73 | const fastestEndpoint = cm._fastestEndpoint; 74 | console.log(`Fastest endpoint: ${fastestEndpoint}`); 75 | })(); 76 | ``` 77 | 78 | ### Fetching the highest slot RPC endpoint 79 | ```typescript 80 | import { ConnectionManager, Logger } from "@solworks/soltoolkit-sdk"; 81 | 82 | (async () => { 83 | // create connection manager 84 | const cm = await ConnectionManager.getInstance({ 85 | commitment: "max", 86 | endpoints: [ 87 | "https://api.devnet.solana.com", 88 | "https://solana-devnet-rpc.allthatnode.com", 89 | "https://mango.devnet.rpcpool.com", 90 | "https://rpc.ankr.com/solana_devnet", 91 | ], 92 | mode: "highest-slot", 93 | network: "devnet" 94 | }); 95 | 96 | // get highest slot endpoint 97 | const highestSlotEndpoint = cm._highestSlotEndpoint; 98 | console.log(`Highest slot endpoint: ${_highestSlotEndpoint}`); 99 | })(); 100 | ``` 101 | 102 | ### Fetching a summary of RPC speeds 103 | ```typescript 104 | import { ConnectionManager, Logger } from "@solworks/soltoolkit-sdk"; 105 | 106 | (async () => { 107 | const logger = new Logger("example"); 108 | 109 | // create connection manager 110 | const cm = await ConnectionManager.getInstance({ 111 | commitment: "max", 112 | endpoints: [ 113 | "https://api.devnet.solana.com", 114 | "https://solana-devnet-rpc.allthatnode.com", 115 | "https://mango.devnet.rpcpool.com", 116 | "https://rpc.ankr.com/solana_devnet", 117 | ], 118 | mode: "fastest", 119 | network: "devnet" 120 | }); 121 | 122 | // get summary of endpoint speeds 123 | const summary = await cm.getEndpointsSummary(); 124 | logger.debug(JSON.stringify(summary, null, 2)); 125 | })(); 126 | ``` 127 | 128 | ### Transfer SOL to 1 user 129 | ```typescript 130 | import { Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 131 | import { 132 | ConnectionManager, 133 | TransactionBuilder, 134 | TransactionWrapper, 135 | Logger 136 | } from "@solworks/soltoolkit-sdk"; 137 | 138 | const logger = new Logger("example"); 139 | const sender = Keypair.generate(); 140 | const receiver = Keypair.generate(); 141 | 142 | (async () => { 143 | // create connection manager 144 | const cm = await ConnectionManager.getInstance({ 145 | commitment: COMMITMENT, 146 | endpoints: [ 147 | "https://api.devnet.solana.com", 148 | "https://solana-devnet-rpc.allthatnode.com", 149 | "https://mango.devnet.rpcpool.com", 150 | "https://rpc.ankr.com/solana_devnet", 151 | ], 152 | mode: "fastest", 153 | network: "devnet", 154 | }); 155 | 156 | // airdrop sol to the generated address 157 | const airdropSig = await cm 158 | .connSync({ airdrop: true }) 159 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 160 | 161 | // confirm airdrop tx 162 | await TransactionWrapper.confirmTx({ 163 | connectionManager: cm, 164 | changeConn: false, 165 | signature: airdropSig, 166 | commitment: "max", 167 | }); 168 | 169 | // create builder and add token transfer ix 170 | var builder = TransactionBuilder 171 | .create() 172 | .addSolTransferIx({ 173 | from: sender.publicKey, 174 | to: receiver.publicKey, 175 | amountLamports: 10_000_000, 176 | }) 177 | .addMemoIx({ 178 | memo: "gm", 179 | signer: sender.publicKey, 180 | }); 181 | 182 | // build the transaction 183 | // returns a transaction with no fee payer or blockhash 184 | let tx = builder.build(); 185 | 186 | // feed transaction into TransactionWrapper 187 | const wrapper = await TransactionWrapper.create({ 188 | connectionManager: cm, 189 | transaction: tx, 190 | signer: sender.publicKey, 191 | }).addBlockhashAndFeePayer(); 192 | 193 | // sign the transaction 194 | const signedTx = await wrapper.sign({ 195 | signer: sender as Signer, 196 | }); 197 | 198 | // send and confirm the transaction 199 | const transferSig = await wrapper.sendAndConfirm({ 200 | serialisedTx: signedTx.serialize(), 201 | }); 202 | })(); 203 | ``` 204 | 205 | ### Send a memo to 1 user 206 | ```typescript 207 | import { Keypair, LAMPORTS_PER_SOL, Signer } from "@solana/web3.js"; 208 | import { 209 | ConnectionManager, 210 | TransactionBuilder, 211 | TransactionWrapper, 212 | Logger 213 | } from "@solworks/soltoolkit-sdk"; 214 | 215 | const logger = new Logger("example"); 216 | const sender = Keypair.generate(); 217 | const receiver = Keypair.generate(); 218 | 219 | (async () => { 220 | // create connection manager 221 | const cm = await ConnectionManager.getInstance({ 222 | commitment: COMMITMENT, 223 | endpoints: [ 224 | "https://api.devnet.solana.com", 225 | "https://solana-devnet-rpc.allthatnode.com", 226 | "https://mango.devnet.rpcpool.com", 227 | "https://rpc.ankr.com/solana_devnet", 228 | ], 229 | mode: "fastest", 230 | network: "devnet", 231 | }); 232 | 233 | // airdrop sol to the generated address 234 | const airdropSig = await cm 235 | .connSync({ airdrop: true }) 236 | .requestAirdrop(sender.publicKey, LAMPORTS_PER_SOL); 237 | 238 | // confirm airdrop tx 239 | await TransactionWrapper.confirmTx({ 240 | connectionManager: cm, 241 | changeConn: false, 242 | signature: airdropSig, 243 | commitment: "max", 244 | }); 245 | 246 | // create builder and add token transfer ix 247 | var builder = TransactionBuilder 248 | .create() 249 | .addMemoIx({ 250 | memo: "gm", 251 | signer: sender.publicKey, 252 | }); 253 | 254 | // build the transaction 255 | // returns a transaction with no fee payer or blockhash 256 | let tx = builder.build(); 257 | 258 | // feed transaction into TransactionWrapper 259 | const wrapper = await TransactionWrapper.create({ 260 | connectionManager: cm, 261 | transaction: tx, 262 | signer: sender.publicKey, 263 | }).addBlockhashAndFeePayer(); 264 | 265 | // sign the transaction 266 | const signedTx = await wrapper.sign({ 267 | signer: sender as Signer, 268 | }); 269 | 270 | // send and confirm the transaction 271 | const transferSig = await wrapper.sendAndConfirm({ 272 | serialisedTx: signedTx.serialize(), 273 | }); 274 | ``` 275 | 276 | ### Dispersing SOL to 10,000 users in <120 seconds 277 | See [example](https://github.com/SolWorks-Dev/soltoolkit-sdk/blob/master/examples/bulk-sol-transfer.ts). 278 | 279 | 280 | ## License 281 | SolToolkit is licensed under [Affero GPL](https://www.gnu.org/licenses/agpl-3.0.txt). -------------------------------------------------------------------------------- /package/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solworks/soltoolkit-sdk", 3 | "version": "0.0.24", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@solworks/soltoolkit-sdk", 9 | "version": "0.0.24", 10 | "license": "Affero GPL", 11 | "dependencies": { 12 | "@solana/buffer-layout": "^4.0.0", 13 | "@solana/spl-token": "^0.3.4", 14 | "@solana/web3.js": "^1.54.0", 15 | "@types/bn.js": "^5.1.0", 16 | "@types/node": "^18.7.13", 17 | "@types/node-fetch": "^2.6.2", 18 | "bn.js": "^5.2.1", 19 | "bs58": "^5.0.0", 20 | "decimal.js": "^10.4.0", 21 | "typescript": "^4.8.2" 22 | }, 23 | "devDependencies": { 24 | "prettier": "^2.7.1", 25 | "typedoc": "^0.23.14" 26 | } 27 | }, 28 | "node_modules/@babel/runtime": { 29 | "version": "7.24.1", 30 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", 31 | "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", 32 | "dependencies": { 33 | "regenerator-runtime": "^0.14.0" 34 | }, 35 | "engines": { 36 | "node": ">=6.9.0" 37 | } 38 | }, 39 | "node_modules/@noble/curves": { 40 | "version": "1.4.0", 41 | "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", 42 | "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", 43 | "dependencies": { 44 | "@noble/hashes": "1.4.0" 45 | }, 46 | "funding": { 47 | "url": "https://paulmillr.com/funding/" 48 | } 49 | }, 50 | "node_modules/@noble/hashes": { 51 | "version": "1.4.0", 52 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", 53 | "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", 54 | "engines": { 55 | "node": ">= 16" 56 | }, 57 | "funding": { 58 | "url": "https://paulmillr.com/funding/" 59 | } 60 | }, 61 | "node_modules/@solana/buffer-layout": { 62 | "version": "4.0.1", 63 | "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", 64 | "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", 65 | "dependencies": { 66 | "buffer": "~6.0.3" 67 | }, 68 | "engines": { 69 | "node": ">=5.10" 70 | } 71 | }, 72 | "node_modules/@solana/buffer-layout-utils": { 73 | "version": "0.2.0", 74 | "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", 75 | "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", 76 | "dependencies": { 77 | "@solana/buffer-layout": "^4.0.0", 78 | "@solana/web3.js": "^1.32.0", 79 | "bigint-buffer": "^1.1.5", 80 | "bignumber.js": "^9.0.1" 81 | }, 82 | "engines": { 83 | "node": ">= 10" 84 | } 85 | }, 86 | "node_modules/@solana/codecs-core": { 87 | "version": "2.0.0-experimental.8618508", 88 | "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-experimental.8618508.tgz", 89 | "integrity": "sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==" 90 | }, 91 | "node_modules/@solana/codecs-data-structures": { 92 | "version": "2.0.0-experimental.8618508", 93 | "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-experimental.8618508.tgz", 94 | "integrity": "sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==", 95 | "dependencies": { 96 | "@solana/codecs-core": "2.0.0-experimental.8618508", 97 | "@solana/codecs-numbers": "2.0.0-experimental.8618508" 98 | } 99 | }, 100 | "node_modules/@solana/codecs-numbers": { 101 | "version": "2.0.0-experimental.8618508", 102 | "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-experimental.8618508.tgz", 103 | "integrity": "sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==", 104 | "dependencies": { 105 | "@solana/codecs-core": "2.0.0-experimental.8618508" 106 | } 107 | }, 108 | "node_modules/@solana/codecs-strings": { 109 | "version": "2.0.0-experimental.8618508", 110 | "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-experimental.8618508.tgz", 111 | "integrity": "sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==", 112 | "dependencies": { 113 | "@solana/codecs-core": "2.0.0-experimental.8618508", 114 | "@solana/codecs-numbers": "2.0.0-experimental.8618508" 115 | }, 116 | "peerDependencies": { 117 | "fastestsmallesttextencoderdecoder": "^1.0.22" 118 | } 119 | }, 120 | "node_modules/@solana/options": { 121 | "version": "2.0.0-experimental.8618508", 122 | "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-experimental.8618508.tgz", 123 | "integrity": "sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==", 124 | "dependencies": { 125 | "@solana/codecs-core": "2.0.0-experimental.8618508", 126 | "@solana/codecs-numbers": "2.0.0-experimental.8618508" 127 | } 128 | }, 129 | "node_modules/@solana/spl-token": { 130 | "version": "0.3.11", 131 | "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", 132 | "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", 133 | "dependencies": { 134 | "@solana/buffer-layout": "^4.0.0", 135 | "@solana/buffer-layout-utils": "^0.2.0", 136 | "@solana/spl-token-metadata": "^0.1.2", 137 | "buffer": "^6.0.3" 138 | }, 139 | "engines": { 140 | "node": ">=16" 141 | }, 142 | "peerDependencies": { 143 | "@solana/web3.js": "^1.88.0" 144 | } 145 | }, 146 | "node_modules/@solana/spl-token-metadata": { 147 | "version": "0.1.2", 148 | "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.2.tgz", 149 | "integrity": "sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==", 150 | "dependencies": { 151 | "@solana/codecs-core": "2.0.0-experimental.8618508", 152 | "@solana/codecs-data-structures": "2.0.0-experimental.8618508", 153 | "@solana/codecs-numbers": "2.0.0-experimental.8618508", 154 | "@solana/codecs-strings": "2.0.0-experimental.8618508", 155 | "@solana/options": "2.0.0-experimental.8618508", 156 | "@solana/spl-type-length-value": "0.1.0" 157 | }, 158 | "engines": { 159 | "node": ">=16" 160 | }, 161 | "peerDependencies": { 162 | "@solana/web3.js": "^1.87.6" 163 | } 164 | }, 165 | "node_modules/@solana/spl-type-length-value": { 166 | "version": "0.1.0", 167 | "resolved": "https://registry.npmjs.org/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz", 168 | "integrity": "sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==", 169 | "dependencies": { 170 | "buffer": "^6.0.3" 171 | }, 172 | "engines": { 173 | "node": ">=16" 174 | } 175 | }, 176 | "node_modules/@solana/web3.js": { 177 | "version": "1.91.3", 178 | "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.91.3.tgz", 179 | "integrity": "sha512-Z6FZyW8SWm7RXW5ZSyr1kmpR+eH/F4DhgxV4WPaq5AbAAMnCiiGm36Jb7ACHFXtWzq1a24hBkJ1wnVANjsmdPA==", 180 | "dependencies": { 181 | "@babel/runtime": "^7.23.4", 182 | "@noble/curves": "^1.2.0", 183 | "@noble/hashes": "^1.3.3", 184 | "@solana/buffer-layout": "^4.0.1", 185 | "agentkeepalive": "^4.5.0", 186 | "bigint-buffer": "^1.1.5", 187 | "bn.js": "^5.2.1", 188 | "borsh": "^0.7.0", 189 | "bs58": "^4.0.1", 190 | "buffer": "6.0.3", 191 | "fast-stable-stringify": "^1.0.0", 192 | "jayson": "^4.1.0", 193 | "node-fetch": "^2.7.0", 194 | "rpc-websockets": "^7.5.1", 195 | "superstruct": "^0.14.2" 196 | } 197 | }, 198 | "node_modules/@solana/web3.js/node_modules/base-x": { 199 | "version": "3.0.9", 200 | "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", 201 | "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", 202 | "dependencies": { 203 | "safe-buffer": "^5.0.1" 204 | } 205 | }, 206 | "node_modules/@solana/web3.js/node_modules/bs58": { 207 | "version": "4.0.1", 208 | "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", 209 | "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", 210 | "dependencies": { 211 | "base-x": "^3.0.2" 212 | } 213 | }, 214 | "node_modules/@types/bn.js": { 215 | "version": "5.1.5", 216 | "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", 217 | "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", 218 | "dependencies": { 219 | "@types/node": "*" 220 | } 221 | }, 222 | "node_modules/@types/connect": { 223 | "version": "3.4.38", 224 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 225 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 226 | "dependencies": { 227 | "@types/node": "*" 228 | } 229 | }, 230 | "node_modules/@types/node": { 231 | "version": "18.19.28", 232 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.28.tgz", 233 | "integrity": "sha512-J5cOGD9n4x3YGgVuaND6khm5x07MMdAKkRyXnjVR6KFhLMNh2yONGiP7Z+4+tBOt5mK+GvDTiacTOVGGpqiecw==", 234 | "dependencies": { 235 | "undici-types": "~5.26.4" 236 | } 237 | }, 238 | "node_modules/@types/node-fetch": { 239 | "version": "2.6.11", 240 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", 241 | "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", 242 | "dependencies": { 243 | "@types/node": "*", 244 | "form-data": "^4.0.0" 245 | } 246 | }, 247 | "node_modules/@types/ws": { 248 | "version": "7.4.7", 249 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", 250 | "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", 251 | "dependencies": { 252 | "@types/node": "*" 253 | } 254 | }, 255 | "node_modules/agentkeepalive": { 256 | "version": "4.5.0", 257 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", 258 | "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", 259 | "dependencies": { 260 | "humanize-ms": "^1.2.1" 261 | }, 262 | "engines": { 263 | "node": ">= 8.0.0" 264 | } 265 | }, 266 | "node_modules/ansi-sequence-parser": { 267 | "version": "1.1.1", 268 | "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", 269 | "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", 270 | "dev": true 271 | }, 272 | "node_modules/asynckit": { 273 | "version": "0.4.0", 274 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 275 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 276 | }, 277 | "node_modules/balanced-match": { 278 | "version": "1.0.2", 279 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 280 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 281 | "dev": true 282 | }, 283 | "node_modules/base-x": { 284 | "version": "4.0.0", 285 | "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", 286 | "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" 287 | }, 288 | "node_modules/base64-js": { 289 | "version": "1.5.1", 290 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 291 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 292 | "funding": [ 293 | { 294 | "type": "github", 295 | "url": "https://github.com/sponsors/feross" 296 | }, 297 | { 298 | "type": "patreon", 299 | "url": "https://www.patreon.com/feross" 300 | }, 301 | { 302 | "type": "consulting", 303 | "url": "https://feross.org/support" 304 | } 305 | ] 306 | }, 307 | "node_modules/bigint-buffer": { 308 | "version": "1.1.5", 309 | "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", 310 | "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", 311 | "hasInstallScript": true, 312 | "dependencies": { 313 | "bindings": "^1.3.0" 314 | }, 315 | "engines": { 316 | "node": ">= 10.0.0" 317 | } 318 | }, 319 | "node_modules/bignumber.js": { 320 | "version": "9.1.2", 321 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", 322 | "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", 323 | "engines": { 324 | "node": "*" 325 | } 326 | }, 327 | "node_modules/bindings": { 328 | "version": "1.5.0", 329 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 330 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 331 | "dependencies": { 332 | "file-uri-to-path": "1.0.0" 333 | } 334 | }, 335 | "node_modules/bn.js": { 336 | "version": "5.2.1", 337 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", 338 | "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" 339 | }, 340 | "node_modules/borsh": { 341 | "version": "0.7.0", 342 | "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", 343 | "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", 344 | "dependencies": { 345 | "bn.js": "^5.2.0", 346 | "bs58": "^4.0.0", 347 | "text-encoding-utf-8": "^1.0.2" 348 | } 349 | }, 350 | "node_modules/borsh/node_modules/base-x": { 351 | "version": "3.0.9", 352 | "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", 353 | "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", 354 | "dependencies": { 355 | "safe-buffer": "^5.0.1" 356 | } 357 | }, 358 | "node_modules/borsh/node_modules/bs58": { 359 | "version": "4.0.1", 360 | "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", 361 | "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", 362 | "dependencies": { 363 | "base-x": "^3.0.2" 364 | } 365 | }, 366 | "node_modules/brace-expansion": { 367 | "version": "2.0.1", 368 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 369 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 370 | "dev": true, 371 | "dependencies": { 372 | "balanced-match": "^1.0.0" 373 | } 374 | }, 375 | "node_modules/bs58": { 376 | "version": "5.0.0", 377 | "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", 378 | "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", 379 | "dependencies": { 380 | "base-x": "^4.0.0" 381 | } 382 | }, 383 | "node_modules/buffer": { 384 | "version": "6.0.3", 385 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 386 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 387 | "funding": [ 388 | { 389 | "type": "github", 390 | "url": "https://github.com/sponsors/feross" 391 | }, 392 | { 393 | "type": "patreon", 394 | "url": "https://www.patreon.com/feross" 395 | }, 396 | { 397 | "type": "consulting", 398 | "url": "https://feross.org/support" 399 | } 400 | ], 401 | "dependencies": { 402 | "base64-js": "^1.3.1", 403 | "ieee754": "^1.2.1" 404 | } 405 | }, 406 | "node_modules/bufferutil": { 407 | "version": "4.0.8", 408 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", 409 | "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", 410 | "hasInstallScript": true, 411 | "optional": true, 412 | "dependencies": { 413 | "node-gyp-build": "^4.3.0" 414 | }, 415 | "engines": { 416 | "node": ">=6.14.2" 417 | } 418 | }, 419 | "node_modules/combined-stream": { 420 | "version": "1.0.8", 421 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 422 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 423 | "dependencies": { 424 | "delayed-stream": "~1.0.0" 425 | }, 426 | "engines": { 427 | "node": ">= 0.8" 428 | } 429 | }, 430 | "node_modules/commander": { 431 | "version": "2.20.3", 432 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 433 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 434 | }, 435 | "node_modules/decimal.js": { 436 | "version": "10.4.3", 437 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", 438 | "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" 439 | }, 440 | "node_modules/delay": { 441 | "version": "5.0.0", 442 | "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", 443 | "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", 444 | "engines": { 445 | "node": ">=10" 446 | }, 447 | "funding": { 448 | "url": "https://github.com/sponsors/sindresorhus" 449 | } 450 | }, 451 | "node_modules/delayed-stream": { 452 | "version": "1.0.0", 453 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 454 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 455 | "engines": { 456 | "node": ">=0.4.0" 457 | } 458 | }, 459 | "node_modules/es6-promise": { 460 | "version": "4.2.8", 461 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 462 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 463 | }, 464 | "node_modules/es6-promisify": { 465 | "version": "5.0.0", 466 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 467 | "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", 468 | "dependencies": { 469 | "es6-promise": "^4.0.3" 470 | } 471 | }, 472 | "node_modules/eventemitter3": { 473 | "version": "4.0.7", 474 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 475 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 476 | }, 477 | "node_modules/eyes": { 478 | "version": "0.1.8", 479 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 480 | "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", 481 | "engines": { 482 | "node": "> 0.1.90" 483 | } 484 | }, 485 | "node_modules/fast-stable-stringify": { 486 | "version": "1.0.0", 487 | "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", 488 | "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" 489 | }, 490 | "node_modules/fastestsmallesttextencoderdecoder": { 491 | "version": "1.0.22", 492 | "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", 493 | "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", 494 | "peer": true 495 | }, 496 | "node_modules/file-uri-to-path": { 497 | "version": "1.0.0", 498 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 499 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 500 | }, 501 | "node_modules/form-data": { 502 | "version": "4.0.0", 503 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 504 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 505 | "dependencies": { 506 | "asynckit": "^0.4.0", 507 | "combined-stream": "^1.0.8", 508 | "mime-types": "^2.1.12" 509 | }, 510 | "engines": { 511 | "node": ">= 6" 512 | } 513 | }, 514 | "node_modules/humanize-ms": { 515 | "version": "1.2.1", 516 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 517 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 518 | "dependencies": { 519 | "ms": "^2.0.0" 520 | } 521 | }, 522 | "node_modules/ieee754": { 523 | "version": "1.2.1", 524 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 525 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 526 | "funding": [ 527 | { 528 | "type": "github", 529 | "url": "https://github.com/sponsors/feross" 530 | }, 531 | { 532 | "type": "patreon", 533 | "url": "https://www.patreon.com/feross" 534 | }, 535 | { 536 | "type": "consulting", 537 | "url": "https://feross.org/support" 538 | } 539 | ] 540 | }, 541 | "node_modules/isomorphic-ws": { 542 | "version": "4.0.1", 543 | "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", 544 | "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", 545 | "peerDependencies": { 546 | "ws": "*" 547 | } 548 | }, 549 | "node_modules/jayson": { 550 | "version": "4.1.0", 551 | "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", 552 | "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", 553 | "dependencies": { 554 | "@types/connect": "^3.4.33", 555 | "@types/node": "^12.12.54", 556 | "@types/ws": "^7.4.4", 557 | "commander": "^2.20.3", 558 | "delay": "^5.0.0", 559 | "es6-promisify": "^5.0.0", 560 | "eyes": "^0.1.8", 561 | "isomorphic-ws": "^4.0.1", 562 | "json-stringify-safe": "^5.0.1", 563 | "JSONStream": "^1.3.5", 564 | "uuid": "^8.3.2", 565 | "ws": "^7.4.5" 566 | }, 567 | "bin": { 568 | "jayson": "bin/jayson.js" 569 | }, 570 | "engines": { 571 | "node": ">=8" 572 | } 573 | }, 574 | "node_modules/jayson/node_modules/@types/node": { 575 | "version": "12.20.55", 576 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", 577 | "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" 578 | }, 579 | "node_modules/json-stringify-safe": { 580 | "version": "5.0.1", 581 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 582 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 583 | }, 584 | "node_modules/jsonc-parser": { 585 | "version": "3.2.1", 586 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", 587 | "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", 588 | "dev": true 589 | }, 590 | "node_modules/jsonparse": { 591 | "version": "1.3.1", 592 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", 593 | "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", 594 | "engines": [ 595 | "node >= 0.2.0" 596 | ] 597 | }, 598 | "node_modules/JSONStream": { 599 | "version": "1.3.5", 600 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", 601 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", 602 | "dependencies": { 603 | "jsonparse": "^1.2.0", 604 | "through": ">=2.2.7 <3" 605 | }, 606 | "bin": { 607 | "JSONStream": "bin.js" 608 | }, 609 | "engines": { 610 | "node": "*" 611 | } 612 | }, 613 | "node_modules/lunr": { 614 | "version": "2.3.9", 615 | "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", 616 | "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", 617 | "dev": true 618 | }, 619 | "node_modules/marked": { 620 | "version": "4.3.0", 621 | "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", 622 | "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", 623 | "dev": true, 624 | "bin": { 625 | "marked": "bin/marked.js" 626 | }, 627 | "engines": { 628 | "node": ">= 12" 629 | } 630 | }, 631 | "node_modules/mime-db": { 632 | "version": "1.52.0", 633 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 634 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 635 | "engines": { 636 | "node": ">= 0.6" 637 | } 638 | }, 639 | "node_modules/mime-types": { 640 | "version": "2.1.35", 641 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 642 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 643 | "dependencies": { 644 | "mime-db": "1.52.0" 645 | }, 646 | "engines": { 647 | "node": ">= 0.6" 648 | } 649 | }, 650 | "node_modules/minimatch": { 651 | "version": "7.4.6", 652 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", 653 | "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", 654 | "dev": true, 655 | "dependencies": { 656 | "brace-expansion": "^2.0.1" 657 | }, 658 | "engines": { 659 | "node": ">=10" 660 | }, 661 | "funding": { 662 | "url": "https://github.com/sponsors/isaacs" 663 | } 664 | }, 665 | "node_modules/ms": { 666 | "version": "2.1.3", 667 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 668 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 669 | }, 670 | "node_modules/node-fetch": { 671 | "version": "2.7.0", 672 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 673 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 674 | "dependencies": { 675 | "whatwg-url": "^5.0.0" 676 | }, 677 | "engines": { 678 | "node": "4.x || >=6.0.0" 679 | }, 680 | "peerDependencies": { 681 | "encoding": "^0.1.0" 682 | }, 683 | "peerDependenciesMeta": { 684 | "encoding": { 685 | "optional": true 686 | } 687 | } 688 | }, 689 | "node_modules/node-gyp-build": { 690 | "version": "4.8.0", 691 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", 692 | "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", 693 | "optional": true, 694 | "bin": { 695 | "node-gyp-build": "bin.js", 696 | "node-gyp-build-optional": "optional.js", 697 | "node-gyp-build-test": "build-test.js" 698 | } 699 | }, 700 | "node_modules/prettier": { 701 | "version": "2.8.8", 702 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", 703 | "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", 704 | "dev": true, 705 | "bin": { 706 | "prettier": "bin-prettier.js" 707 | }, 708 | "engines": { 709 | "node": ">=10.13.0" 710 | }, 711 | "funding": { 712 | "url": "https://github.com/prettier/prettier?sponsor=1" 713 | } 714 | }, 715 | "node_modules/regenerator-runtime": { 716 | "version": "0.14.1", 717 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", 718 | "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" 719 | }, 720 | "node_modules/rpc-websockets": { 721 | "version": "7.9.0", 722 | "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.9.0.tgz", 723 | "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", 724 | "dependencies": { 725 | "@babel/runtime": "^7.17.2", 726 | "eventemitter3": "^4.0.7", 727 | "uuid": "^8.3.2", 728 | "ws": "^8.5.0" 729 | }, 730 | "funding": { 731 | "type": "paypal", 732 | "url": "https://paypal.me/kozjak" 733 | }, 734 | "optionalDependencies": { 735 | "bufferutil": "^4.0.1", 736 | "utf-8-validate": "^5.0.2" 737 | } 738 | }, 739 | "node_modules/rpc-websockets/node_modules/ws": { 740 | "version": "8.16.0", 741 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", 742 | "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", 743 | "engines": { 744 | "node": ">=10.0.0" 745 | }, 746 | "peerDependencies": { 747 | "bufferutil": "^4.0.1", 748 | "utf-8-validate": ">=5.0.2" 749 | }, 750 | "peerDependenciesMeta": { 751 | "bufferutil": { 752 | "optional": true 753 | }, 754 | "utf-8-validate": { 755 | "optional": true 756 | } 757 | } 758 | }, 759 | "node_modules/safe-buffer": { 760 | "version": "5.2.1", 761 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 762 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 763 | "funding": [ 764 | { 765 | "type": "github", 766 | "url": "https://github.com/sponsors/feross" 767 | }, 768 | { 769 | "type": "patreon", 770 | "url": "https://www.patreon.com/feross" 771 | }, 772 | { 773 | "type": "consulting", 774 | "url": "https://feross.org/support" 775 | } 776 | ] 777 | }, 778 | "node_modules/shiki": { 779 | "version": "0.14.7", 780 | "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", 781 | "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", 782 | "dev": true, 783 | "dependencies": { 784 | "ansi-sequence-parser": "^1.1.0", 785 | "jsonc-parser": "^3.2.0", 786 | "vscode-oniguruma": "^1.7.0", 787 | "vscode-textmate": "^8.0.0" 788 | } 789 | }, 790 | "node_modules/superstruct": { 791 | "version": "0.14.2", 792 | "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", 793 | "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" 794 | }, 795 | "node_modules/text-encoding-utf-8": { 796 | "version": "1.0.2", 797 | "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", 798 | "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" 799 | }, 800 | "node_modules/through": { 801 | "version": "2.3.8", 802 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 803 | "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 804 | }, 805 | "node_modules/tr46": { 806 | "version": "0.0.3", 807 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 808 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 809 | }, 810 | "node_modules/typedoc": { 811 | "version": "0.23.28", 812 | "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", 813 | "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", 814 | "dev": true, 815 | "dependencies": { 816 | "lunr": "^2.3.9", 817 | "marked": "^4.2.12", 818 | "minimatch": "^7.1.3", 819 | "shiki": "^0.14.1" 820 | }, 821 | "bin": { 822 | "typedoc": "bin/typedoc" 823 | }, 824 | "engines": { 825 | "node": ">= 14.14" 826 | }, 827 | "peerDependencies": { 828 | "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" 829 | } 830 | }, 831 | "node_modules/typescript": { 832 | "version": "4.9.5", 833 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 834 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 835 | "bin": { 836 | "tsc": "bin/tsc", 837 | "tsserver": "bin/tsserver" 838 | }, 839 | "engines": { 840 | "node": ">=4.2.0" 841 | } 842 | }, 843 | "node_modules/undici-types": { 844 | "version": "5.26.5", 845 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 846 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 847 | }, 848 | "node_modules/utf-8-validate": { 849 | "version": "5.0.10", 850 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", 851 | "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", 852 | "hasInstallScript": true, 853 | "optional": true, 854 | "dependencies": { 855 | "node-gyp-build": "^4.3.0" 856 | }, 857 | "engines": { 858 | "node": ">=6.14.2" 859 | } 860 | }, 861 | "node_modules/uuid": { 862 | "version": "8.3.2", 863 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 864 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 865 | "bin": { 866 | "uuid": "dist/bin/uuid" 867 | } 868 | }, 869 | "node_modules/vscode-oniguruma": { 870 | "version": "1.7.0", 871 | "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", 872 | "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", 873 | "dev": true 874 | }, 875 | "node_modules/vscode-textmate": { 876 | "version": "8.0.0", 877 | "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", 878 | "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", 879 | "dev": true 880 | }, 881 | "node_modules/webidl-conversions": { 882 | "version": "3.0.1", 883 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 884 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 885 | }, 886 | "node_modules/whatwg-url": { 887 | "version": "5.0.0", 888 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 889 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 890 | "dependencies": { 891 | "tr46": "~0.0.3", 892 | "webidl-conversions": "^3.0.0" 893 | } 894 | }, 895 | "node_modules/ws": { 896 | "version": "7.5.9", 897 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", 898 | "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", 899 | "engines": { 900 | "node": ">=8.3.0" 901 | }, 902 | "peerDependencies": { 903 | "bufferutil": "^4.0.1", 904 | "utf-8-validate": "^5.0.2" 905 | }, 906 | "peerDependenciesMeta": { 907 | "bufferutil": { 908 | "optional": true 909 | }, 910 | "utf-8-validate": { 911 | "optional": true 912 | } 913 | } 914 | } 915 | } 916 | } 917 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solworks/soltoolkit-sdk", 3 | "version": "0.0.33", 4 | "description": "SolToolkit SDK, by SolWorks. A set of tools by developers for developers.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "repository": "https://github.com/SolWorks-Dev/soltoolkit-sdk", 8 | "files": [ 9 | "/build" 10 | ], 11 | "scripts": { 12 | "clean": "rm -rf ./build", 13 | "build": "npx tsc", 14 | "build::publish::patch": "npm run build && npm version patch && npm publish --access=public", 15 | "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", 16 | "publish": "npm publish --access=public" 17 | }, 18 | "keywords": [], 19 | "author": "Zhe SolWorks", 20 | "license": "Affero GPL", 21 | "dependencies": { 22 | "@solana/buffer-layout": "^4.0.0", 23 | "@solana/spl-token": "^0.3.4", 24 | "@solana/web3.js": "^1.54.0", 25 | "@types/bn.js": "^5.1.0", 26 | "@types/node": "^18.7.13", 27 | "@types/node-fetch": "^2.6.2", 28 | "bn.js": "^5.2.1", 29 | "bs58": "^5.0.0", 30 | "decimal.js": "^10.4.0", 31 | "typescript": "^4.8.2" 32 | }, 33 | "devDependencies": { 34 | "prettier": "^2.7.1", 35 | "typedoc": "^0.23.14" 36 | } 37 | } -------------------------------------------------------------------------------- /package/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBuilder } from './modules/TransactionBuilder'; 2 | import { TransactionWrapper, getJitoEndpoint, sendTxUsingJito } from './modules/TransactionWrapper'; 3 | import { SingleTransactionWrapper } from './modules/SingleTransactionWrapper'; 4 | import { ConnectionManager, IConnectionManagerConstructor, IRPCSummary, Mode } from './modules/ConnectionManager'; 5 | import { Disperse, TokenType, IDisperseConstructor } from './modules/Disperse'; 6 | import { ITransfer } from './interfaces/ITransfer'; 7 | import { Logger } from './modules/Logger'; 8 | import { TransactionHelper } from './modules/TransactionHelper'; 9 | import SNSDomainResolver from './modules/SNSDomainResolver'; 10 | export { 11 | TransactionBuilder, 12 | TransactionWrapper, 13 | ConnectionManager, 14 | Disperse, 15 | TokenType, 16 | IDisperseConstructor, 17 | IConnectionManagerConstructor, 18 | IRPCSummary, 19 | Mode, 20 | ITransfer, 21 | Logger, 22 | TransactionHelper, 23 | SNSDomainResolver, 24 | getJitoEndpoint, 25 | sendTxUsingJito, 26 | SingleTransactionWrapper 27 | }; 28 | -------------------------------------------------------------------------------- /package/src/interfaces/ILogger.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | debug(...args: any[]): void; 3 | info(...args: any[]): void; 4 | warn(...args: any[]): void; 5 | error(...args: any[]): void; 6 | makeError(...args: any[]): Error; 7 | } 8 | -------------------------------------------------------------------------------- /package/src/interfaces/ITransfer.ts: -------------------------------------------------------------------------------- 1 | export interface ITransfer { 2 | recipient: string; 3 | amount: number; // lamports 4 | associatedTokenAccount?: string; // optional for spl 5 | } -------------------------------------------------------------------------------- /package/src/interfaces/IWallet.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, PublicKey } from '@solana/web3.js'; 2 | export interface IWallet { 3 | signTransaction(tx: Transaction): Promise | undefined; 4 | signAllTransactions(txs: Transaction[]): Promise | undefined; 5 | publicKey: PublicKey | null; 6 | } -------------------------------------------------------------------------------- /package/src/modules/ConnectionManager.ts: -------------------------------------------------------------------------------- 1 | import { Cluster, Commitment, Connection, ConnectionConfig } from '@solana/web3.js'; 2 | import { ILogger } from '../interfaces/ILogger'; 3 | import { Logger } from './Logger'; 4 | 5 | /** 6 | * Manager for one or more web3.js connection(s). 7 | * 8 | * @remarks 9 | * This class is a singleton. Use the `getInstance()` method to get the instance. 10 | * 11 | * @beta 12 | * 13 | * @example 14 | * ```typescript 15 | * import { ConnectionManager } from "@solworks/soltoolkit-sdk"; 16 | * 17 | * (async () => { 18 | * const cm = await ConnectionManager.getInstance({ 19 | * commitment: COMMITMENT, 20 | * endpoints: [ 21 | * "https://mango.devnet.rpcpool.com", 22 | * "https://api.devnet.solana.com", 23 | * "https://devnet.genesysgo.net", 24 | * ], 25 | * mode: "round-robin", 26 | * network: "devnet", 27 | * }); 28 | * }) 29 | * ``` 30 | */ 31 | export class ConnectionManager { 32 | private static _instance: ConnectionManager; 33 | public _connection: Connection; 34 | public _fastestEndpoint: string; 35 | public _highestSlotEndpoint: string; 36 | public _latestValidBlockHeightEndpoint: string; 37 | private _config: IConnectionManagerConstructor; 38 | private _logger: ILogger = new Logger('@soltoolkit/ConnectionManager'); 39 | private _rpcSummary: IRPCSummary[] = []; 40 | 41 | private constructor( 42 | { 43 | network = 'mainnet-beta', 44 | endpoint, 45 | config, 46 | commitment = 'processed', 47 | endpoints, 48 | mode = 'single', 49 | rpcSummary: endpointsSortedBySpeed, 50 | verbose = false, 51 | transactionTimeout = 120_000 52 | }: IConnectionManagerConstructor, 53 | ) { 54 | let rpcUrl: string | undefined; 55 | 56 | if (verbose) 57 | this._logger.debug( 58 | `Initializing ConnectionManager with params: ${JSON.stringify( 59 | { 60 | network, 61 | endpoint, 62 | config, 63 | commitment, 64 | endpoints, 65 | mode, 66 | endpointsSortedBySpeed 67 | }, 68 | null, 69 | 2 70 | )}` 71 | ); 72 | 73 | // filter out unreachable endpoints 74 | const reachableEndpoints = endpointsSortedBySpeed.filter((endpoint) => endpoint.isReachable === true); 75 | 76 | // filter by speed, ascending (lowest first) 77 | const fastestEndpoint = reachableEndpoints.sort((a, b) => a.speedMs! - b.speedMs!)[0].endpoint; 78 | 79 | // filter by slot height, descending (highest first) 80 | const highestSlotEndpoint = reachableEndpoints.sort((a, b) => b.currentSlot! - a.currentSlot!)[0].endpoint; 81 | 82 | // filter by last valid block height, descending (highest first) 83 | const latestValidBlockHeightEndpoint = reachableEndpoints.sort((a, b) => b.lastValidBlockHeight! - a.lastValidBlockHeight!)[0].endpoint; 84 | 85 | // set rpc url based on mode and network 86 | switch (mode) { 87 | // priority for endpoint over endpoints array 88 | // fallback to endpoints array if endpoint is not provided 89 | // fallback to default endpoint if no endpoint or endpoints provided 90 | case 'single': 91 | { 92 | if (endpoint) { 93 | rpcUrl = endpoint; 94 | } else if (endpoints && endpoints.length > 0) { 95 | rpcUrl = endpoints[0]; 96 | } else { 97 | rpcUrl = ConnectionManager.getDefaultEndpoint(network); 98 | } 99 | } 100 | break; 101 | // uses endpoints array only, first item selected 102 | // no fallback support if endpoints array is not provided 103 | case 'first': 104 | { 105 | if (endpoints && endpoints.length > 0) { 106 | rpcUrl = endpoints[0]; 107 | } else { 108 | throw new Error('No endpoints provided with mode "first"'); 109 | } 110 | } 111 | break; 112 | // uses endpoints array only, last item selected 113 | // no fallback support if endpoints array is not provided 114 | case 'last': 115 | { 116 | if (endpoints && endpoints.length > 0) { 117 | rpcUrl = endpoints[-1]; 118 | } else { 119 | throw new Error('No endpoints provided with mode "last"'); 120 | } 121 | } 122 | break; 123 | // uses endpoints array only, alternates between endpoints 124 | // starts with first item in array 125 | // no fallback support if endpoints array is not provided 126 | case 'round-robin': 127 | { 128 | if (endpoints && endpoints.length > 0) { 129 | rpcUrl = endpoints[0]; 130 | } else { 131 | throw new Error('No endpoints provided with mode "round-robin"'); 132 | } 133 | } 134 | break; 135 | // uses the fastest endpoint determined in the static initialization 136 | // no fallback support 137 | case 'fastest': { 138 | if (fastestEndpoint) { 139 | rpcUrl = fastestEndpoint; 140 | } else { 141 | throw new Error('No fastest endpoint provided with mode "fastest"'); 142 | } 143 | break; 144 | } 145 | // uses the highest slot endpoint determined in the static initialization 146 | // no fallback support 147 | case 'highest-slot': 148 | { 149 | if (highestSlotEndpoint) { 150 | rpcUrl = highestSlotEndpoint; 151 | } else { 152 | throw new Error('No highest slot endpoint provided with mode "highest-slot"'); 153 | } 154 | } 155 | break; 156 | // uses the endpoint with the latest valid block height 157 | // no fallback support 158 | case 'latest-valid-block-height': 159 | { 160 | if (latestValidBlockHeightEndpoint) { 161 | rpcUrl = latestValidBlockHeightEndpoint; 162 | } else { 163 | throw new Error('No latest valid block height endpoint provided with mode "latest-valid-block-height"'); 164 | } 165 | } 166 | break; 167 | // uses endpoints array only, selects random item from array 168 | // no fallback support if endpoints array is not provided 169 | case 'random': 170 | { 171 | if (endpoints && endpoints.length > 0) { 172 | rpcUrl = endpoints[Math.floor(Math.random() * endpoints.length)]; 173 | } else { 174 | throw new Error('No endpoints provided with mode "random"'); 175 | } 176 | } 177 | break; 178 | default: 179 | throw new Error('Invalid mode'); 180 | } 181 | 182 | if (rpcUrl === undefined) { 183 | throw new Error('No endpoint has been set'); 184 | } 185 | 186 | if (verbose) this._logger.debug(`Using endpoint: ${rpcUrl}`); 187 | 188 | this._connection = new Connection(rpcUrl, { 189 | ...config, 190 | commitment, 191 | confirmTransactionInitialTimeout: 120_000 192 | }); 193 | this._config = { 194 | network, 195 | endpoint, 196 | config, 197 | commitment, 198 | endpoints, 199 | mode, 200 | rpcSummary: endpointsSortedBySpeed, 201 | verbose, 202 | transactionTimeout 203 | }; 204 | this._fastestEndpoint = fastestEndpoint || rpcUrl; 205 | this._highestSlotEndpoint = highestSlotEndpoint || rpcUrl; 206 | this._latestValidBlockHeightEndpoint = latestValidBlockHeightEndpoint || rpcUrl; 207 | this._rpcSummary = endpointsSortedBySpeed; 208 | } 209 | 210 | /** 211 | * Builds and returns a singleton instance of the ConnectionManager class. This method runs a speed test on the provided endpoint/s on initialization. 212 | * @param {Cluster} values.network - The network to connect to. 213 | * @param {string=} values.endpoint - If using `mode` "single", will default to this endpoint. If not provided, will default to the default public RPC endpoint for the network. 214 | * @param {string[]=} values.endpoints - If any other mode, will default to this array of endpoints. If not provided, will default to `values.endpoint` or the default public RPC endpoint for the network. 215 | * @param {ConnectionConfig=} values.config - Additional configuration options for the web3.js connection. 216 | * @param {Commitment=} values.commitment - The commitment level. Defaults to "processed". 217 | * @param {Mode=} values.mode - The mode to use for selecting an endpoint. 218 | * @returns {ConnectionManager} A singleton instance of the ConnectionManager class. 219 | */ 220 | public static async getInstance(values: Omit): Promise { 221 | if (!ConnectionManager._instance) { 222 | const endpoints = values.endpoints 223 | ? values.endpoints 224 | : values.endpoint !== undefined 225 | ? [values.endpoint] 226 | : [this.getDefaultEndpoint(values.network)]; 227 | const endpointsSummary = await ConnectionManager.getEndpointsSummary( 228 | endpoints, 229 | values.commitment || 'processed' 230 | ); 231 | 232 | // if no endpoints are available, throw error 233 | if (endpointsSummary.every((endpoint) => endpoint.isReachable === false)) { 234 | throw new Error('No reachable endpoints'); 235 | } 236 | 237 | // check if any endpoints are available 238 | const endpointsSortedBySpeed = endpointsSummary 239 | .filter((endpoint) => endpoint.isReachable === true) 240 | .sort((a, b) => a.speedMs! - b.speedMs!); 241 | ConnectionManager._instance = new ConnectionManager({ 242 | ...values, 243 | rpcSummary: endpointsSortedBySpeed 244 | }); 245 | } 246 | 247 | return ConnectionManager._instance; 248 | } 249 | 250 | /** 251 | * Builds and returns a singleton instance of the ConnectionManager class. This method should only be used after initializing the ConnectionManager with `getInstance()`. 252 | * @returns {Connection} The web3.js connection. 253 | */ 254 | public static getInstanceSync(): ConnectionManager { 255 | if (!ConnectionManager._instance) { 256 | throw new Error('ConnectionManager has not been initialized'); 257 | } 258 | 259 | return ConnectionManager._instance; 260 | } 261 | 262 | /** 263 | * Returns a web3.js connection. 264 | * 265 | * @remarks 266 | * If you are using `mode` "fastest" or "highest-slot", this method will return the RPC determined during initialization of ConnectionManager. Use the async `conn()` method instead to update the determined RPC. 267 | * 268 | * @param changeConn - If true, will return a new connection based on the configured `mode`. If false, will return the current connection. 269 | * @param airdrop - If true, will default to the public RPC endpoint hosted by Solana (it is the only RPC endpoint that supports airdrops). 270 | * @returns A web3.js connection. 271 | */ 272 | public connSync({ changeConn = true, airdrop = false }: { changeConn?: boolean; airdrop?: boolean }): Connection { 273 | if (!changeConn) { 274 | return this._connection; 275 | } 276 | 277 | let conn: Connection = this._connection; 278 | 279 | if (airdrop) { 280 | conn = new Connection( 281 | ConnectionManager.getDefaultEndpoint(this._config.network), 282 | this._config.config || this._config.commitment 283 | ); 284 | } else { 285 | switch (this._config.mode) { 286 | case 'single': 287 | case 'first': 288 | case 'last': 289 | { 290 | // handled in constructor, no need to reinitialize 291 | // use async method to get new connection for `fastest` or `hightest-slot` mode 292 | conn = this._connection; 293 | } 294 | break; 295 | case 'highest-slot': 296 | { 297 | if (this._connection.rpcEndpoint !== this._highestSlotEndpoint) { 298 | if (this._config.verbose) this._logger.debug(`Changing endpoint to ${this._highestSlotEndpoint}`); 299 | conn = new Connection( 300 | this._highestSlotEndpoint, 301 | this._config.config || this._config.commitment 302 | ); 303 | } 304 | } 305 | break; 306 | case 'fastest': { 307 | { 308 | if (this._connection.rpcEndpoint !== this._fastestEndpoint) { 309 | if (this._config.verbose) this._logger.debug(`Changing connection to ${this._fastestEndpoint}`); 310 | conn = new Connection( 311 | this._fastestEndpoint, 312 | this._config.config || this._config.commitment 313 | ); 314 | } 315 | } 316 | break; 317 | } 318 | case 'round-robin': 319 | { 320 | const currentIndex = this._config.endpoints?.indexOf(this._connection.rpcEndpoint); 321 | if (currentIndex === -1) { 322 | if ( 323 | this._connection.rpcEndpoint === 324 | ConnectionManager.getDefaultEndpoint(this._config.network) 325 | ) { 326 | conn = new Connection( 327 | this._config.endpoints![0], 328 | this._config.config || this._config.commitment 329 | ); 330 | } else { 331 | throw new Error('Current endpoint not found in endpoints array'); 332 | } 333 | } else if (currentIndex !== undefined) { 334 | // we can assume endpoints is non-null at this point 335 | // constructor will throw if endpoints is null + mode is round-robin 336 | const nextIndex = currentIndex + 1 >= this._config.endpoints!.length ? 0 : currentIndex + 1; 337 | const rpcUrl = this._config.endpoints![nextIndex]; 338 | conn = new Connection(rpcUrl, this._config.config || this._config.commitment); 339 | } else { 340 | throw new Error('Current index is undefined'); 341 | } 342 | } 343 | break; 344 | case 'latest-valid-block-height': 345 | { 346 | if (this._connection.rpcEndpoint !== this._latestValidBlockHeightEndpoint) { 347 | if (this._config.verbose) this._logger.debug(`Changing connection to ${this._latestValidBlockHeightEndpoint}`); 348 | conn = new Connection( 349 | this._latestValidBlockHeightEndpoint, 350 | this._config.config || this._config.commitment 351 | ); 352 | } 353 | } 354 | break; 355 | case 'random': 356 | { 357 | const rpcUrl = 358 | this._config.endpoints![Math.floor(Math.random() * this._config.endpoints!.length)]; 359 | conn = new Connection(rpcUrl, this._config.config || this._config.commitment); 360 | } 361 | break; 362 | default: 363 | if (this._config.verbose) this._logger.error('Invalid mode'); 364 | conn = this._connection; 365 | break; 366 | } 367 | } 368 | 369 | if (this._config.verbose) this._logger.debug(`Using endpoint: ${conn.rpcEndpoint}`); 370 | this._connection = conn; 371 | return conn; 372 | } 373 | 374 | /** 375 | * Returns a web3.js connection. 376 | * 377 | * @param changeConn - If true, will return a new connection based on the configured `mode`. If false, will return the current connection. 378 | * @param airdrop - If true, will default to the public RPC endpoint hosted by Solana (it is the only RPC endpoint that supports airdrops). 379 | * @returns A web3.js connection. 380 | */ 381 | public async conn({ 382 | changeConn = true, 383 | airdrop = false 384 | }: { 385 | changeConn?: boolean; 386 | airdrop?: boolean; 387 | }): Promise { 388 | if (!changeConn) { 389 | return this._connection; 390 | } 391 | 392 | let conn: Connection = this._connection; 393 | 394 | if (airdrop) { 395 | conn = new Connection( 396 | ConnectionManager.getDefaultEndpoint(this._config.network), 397 | this._config.config || this._config.commitment 398 | ); 399 | } else { 400 | switch (this._config.mode) { 401 | case 'single': 402 | case 'first': 403 | case 'last': 404 | // handled in constructor, no need to reinitialize 405 | conn = this._connection; 406 | break; 407 | case 'highest-slot': 408 | { 409 | const endpointsSummary = await this.getEndpointsSummary(); 410 | 411 | // throw error if all endpoints are unreachable 412 | if (endpointsSummary.every((endpoint) => endpoint.isReachable === false)) { 413 | throw new Error('All endpoints unreachable'); 414 | } 415 | 416 | // filter out unreachable endpoints 417 | let reachableEndpoints = endpointsSummary 418 | .filter((endpoint) => endpoint.isReachable === true) 419 | .sort((a, b) => a.currentSlot! - b.currentSlot!); 420 | 421 | const highestSlotEndpoint = reachableEndpoints[0].endpoint; 422 | if (this._connection.rpcEndpoint !== highestSlotEndpoint) { 423 | if (this._config.verbose) this._logger.debug(`Changing endpoint to ${highestSlotEndpoint}`); 424 | conn = new Connection(highestSlotEndpoint, this._config.config || this._config.commitment); 425 | } 426 | } 427 | break; 428 | case 'fastest': { 429 | { 430 | const endpointsSummary = await this.getEndpointsSummary(); 431 | 432 | // throw error if all endpoints are unreachable 433 | if (endpointsSummary.every((endpoint) => endpoint.isReachable === false)) { 434 | throw new Error('All endpoints unreachable'); 435 | } 436 | 437 | // filter out unreachable endpoints 438 | let reachableEndpoints = endpointsSummary 439 | .filter((endpoint) => endpoint.isReachable === true) 440 | .sort((a, b) => a.speedMs! - b.speedMs!); 441 | 442 | const fastestEndpoint = reachableEndpoints[0].endpoint; 443 | if (this._connection.rpcEndpoint !== fastestEndpoint) { 444 | if (this._config.verbose) this._logger.debug(`Changing connection to ${fastestEndpoint}`); 445 | conn = new Connection(fastestEndpoint, this._config.config || this._config.commitment); 446 | } 447 | } 448 | break; 449 | } 450 | case 'round-robin': 451 | { 452 | const currentIndex = this._config.endpoints?.indexOf(this._connection.rpcEndpoint); 453 | if (currentIndex === -1) { 454 | if ( 455 | this._connection.rpcEndpoint === 456 | ConnectionManager.getDefaultEndpoint(this._config.network) 457 | ) { 458 | conn = new Connection( 459 | this._config.endpoints![0], 460 | this._config.config || this._config.commitment 461 | ); 462 | } else { 463 | throw new Error('Current endpoint not found in endpoints array'); 464 | } 465 | } else if (currentIndex !== undefined) { 466 | // we can assume endpoints is non-null at this point 467 | // constructor will throw if endpoints is null + mode is round-robin 468 | const nextIndex = currentIndex + 1 >= this._config.endpoints!.length ? 0 : currentIndex + 1; 469 | const rpcUrl = this._config.endpoints![nextIndex]; 470 | conn = new Connection(rpcUrl, this._config.config || this._config.commitment); 471 | } else { 472 | throw new Error('Current index is undefined'); 473 | } 474 | } 475 | break; 476 | case 'random': 477 | const rpcUrl = this._config.endpoints![Math.floor(Math.random() * this._config.endpoints!.length)]; 478 | conn = new Connection(rpcUrl, this._config.config || this._config.commitment); 479 | break; 480 | case 'latest-valid-block-height': 481 | { 482 | // get endpoint summary 483 | const endpointsSummary = await this.getEndpointsSummary(); 484 | // throw error if all endpoints are unreachable 485 | if (endpointsSummary.every((endpoint) => endpoint.isReachable === false)) { 486 | throw new Error('All endpoints unreachable'); 487 | } 488 | // filter unreachable endpoints and sort by last valid block height 489 | let reachableEndpoints = endpointsSummary 490 | .filter((endpoint) => endpoint.isReachable === true) 491 | .sort((a, b) => b.lastValidBlockHeight! - a.lastValidBlockHeight!); 492 | const latestValidBlockHeightEndpoint = reachableEndpoints[0].endpoint; 493 | if (this._connection.rpcEndpoint !== latestValidBlockHeightEndpoint) { 494 | if (this._config.verbose) this._logger.debug(`Changing connection to ${latestValidBlockHeightEndpoint}`); 495 | conn = new Connection( 496 | latestValidBlockHeightEndpoint, 497 | this._config.config || this._config.commitment 498 | ); 499 | } 500 | } 501 | default: 502 | if (this._config.verbose) this._logger.error('Invalid mode'); 503 | conn = this._connection; 504 | break; 505 | } 506 | } 507 | 508 | if (this._config.verbose) this._logger.debug(`Using endpoint: ${conn.rpcEndpoint}`); 509 | this._connection = conn; 510 | return conn; 511 | } 512 | 513 | /** 514 | * Returns a summary of speed and slot height for each endpoint. 515 | * @returns {Promise} An array of IRPCSummary objects. 516 | */ 517 | public async getEndpointsSummary(): Promise { 518 | const endpoints = this._config.endpoints || [this._connection.rpcEndpoint]; 519 | const summary = await ConnectionManager.getEndpointsSummary(endpoints); 520 | this._rpcSummary = summary; 521 | return summary; 522 | } 523 | 524 | public getRpcSummary(): IRPCSummary[] { 525 | return this._rpcSummary; 526 | } 527 | 528 | /** 529 | * A static version of `getEndpointsSummary()`. Returns a summary of speed and slot height for each endpoint. 530 | * @param endpoints - An array of endpoints to test. 531 | * @param commitment - The commitment level. 532 | * @returns {Promise} An array of IRPCSummary objects. 533 | */ 534 | public static async getEndpointsSummary(endpoints: string[], commitment?: Commitment): Promise { 535 | // handle if endpoints is empty 536 | if (endpoints.length === 0) { 537 | throw new Error('Endpoints array is empty'); 538 | } 539 | 540 | // no handling if endpoint is unavailable 541 | const results = await Promise.all( 542 | endpoints.map(async (endpoint) => { 543 | try { 544 | const conn = new Connection(endpoint); 545 | const start = Date.now(); 546 | const { context, value } = await conn.getLatestBlockhashAndContext(commitment); 547 | const end = Date.now(); 548 | const speedMs = end - start; 549 | return { 550 | endpoint, 551 | speedMs, 552 | currentSlot: context.slot, 553 | isReachable: true, 554 | lastValidBlockHeight: value.lastValidBlockHeight 555 | } as IRPCSummary; 556 | } catch { 557 | return { 558 | endpoint, 559 | speedMs: undefined, 560 | currentSlot: undefined, 561 | isReachable: false, 562 | lastValidBlockHeight: undefined 563 | } as IRPCSummary; 564 | } 565 | }) 566 | ); 567 | 568 | return results; 569 | } 570 | 571 | /** 572 | * Returns the fastest endpoint url, speed and slot height. 573 | * @param endpoints - An array of endpoints to test. 574 | * @param commitment - The commitment level. 575 | * @returns {Promise} An IRPCSummary object. 576 | */ 577 | public static async getFastestEndpoint(endpoints: string[], commitment?: Commitment): Promise { 578 | let summary = await ConnectionManager.getEndpointsSummary(endpoints, commitment); 579 | 580 | // if all endpoints are unreachable, throw error 581 | if (summary.every((endpoint) => endpoint.isReachable === false)) { 582 | throw new Error('No reachable endpoints'); 583 | } 584 | 585 | // filter out unreachable endpoints 586 | let reachableEndpoints = summary 587 | .filter((endpoint) => endpoint.isReachable === true) 588 | .sort((a, b) => a.speedMs! - b.speedMs!); 589 | 590 | return reachableEndpoints[0]; 591 | } 592 | 593 | /** 594 | * Returns the default endpoint for the given network. 595 | * @param network - The network to get the default endpoint for. 596 | * @returns {string} The default endpoint. 597 | */ 598 | public static getDefaultEndpoint(network: string | undefined): string { 599 | switch (network) { 600 | case 'mainnet-beta': 601 | return 'https://api.mainnet-beta.solana.com'; 602 | case 'testnet': 603 | return 'https://api.testnet.solana.com'; 604 | case 'devnet': 605 | return 'https://api.devnet.solana.com'; 606 | case 'localnet': 607 | return 'http://localhost:8899'; 608 | default: 609 | throw new Error(`Invalid network: ${network}`); 610 | } 611 | } 612 | } 613 | 614 | /** 615 | * The constructor for the ConnectionManager class. 616 | * @param {Cluster} values.network - The network to connect to. 617 | * @param {string=} values.endpoint - If using `mode` "single", will default to this endpoint. If not provided, will default to the default public RPC endpoint for the network. 618 | * @param {string[]=} values.endpoints - If any other mode, will default to this array of endpoints. If not provided, will default to `values.endpoint` or the default public RPC endpoint for the network. 619 | * @param {ConnectionConfig=} values.config - Additional configuration options for the web3.js connection. 620 | * @param {Commitment=} values.commitment - The commitment level. Defaults to "processed". 621 | * @param {Mode=} values.mode - The mode to use for selecting an endpoint. 622 | * @param {IRPCSummary[]} values.rpcSummary - An array of IRPCSummary objects. 623 | * @param {boolean=} values.verbose - Whether to log initialization details. 624 | * @param {number=} values.transactionTimeout - The transaction timeout in milliseconds. 625 | */ 626 | export interface IConnectionManagerConstructor { 627 | network: Cluster; 628 | endpoint?: string; 629 | endpoints?: string[]; 630 | config?: ConnectionConfig; 631 | commitment?: Commitment; 632 | mode?: Mode; 633 | rpcSummary: IRPCSummary[]; 634 | verbose?: boolean; 635 | transactionTimeout?: number; 636 | } 637 | 638 | /** 639 | * An object representing a summary of speed and slot height for an endpoint. 640 | * @param {string} endpoint - The endpoint url. 641 | * @param {boolean} isReachable - Whether the endpoint is reachable. 642 | * @param {number=} speedMs - The speed of the endpoint in milliseconds. 643 | * @param {number=} currentSlot - The current slot height of the endpoint. 644 | * @param {string=} lastValidBlockHeight - The last valid block height of the endpoint. 645 | */ 646 | export interface IRPCSummary { 647 | endpoint: string; 648 | isReachable: boolean; 649 | speedMs?: number; 650 | currentSlot?: number; 651 | lastValidBlockHeight?: number; 652 | } 653 | 654 | /** 655 | * The mode to use for selecting an endpoint. 656 | * @param {string} single - Priority for endpoint over endpoints array. Fallback to endpoints array if endpoint is not provided. Fallback to default endpoint if no endpoint or endpoints provided. 657 | * @param {string} first - Uses endpoints array only, first item selected. No fallback support if endpoints array is not provided. 658 | * @param {string} last - Uses endpoints array only, last item selected. No fallback support if endpoints array is not provided. 659 | * @param {string} round-robin - Uses endpoints array only, alternates between endpoints. Starts with first item in array. No fallback support if endpoints array is not provided. 660 | * @param {string} fastest - Uses the fastest endpoint determined in the static initialization. No fallback support. 661 | * @param {string} highest-slot - Uses the highest slot endpoint determined in the static initialization. No fallback support. 662 | * @param {string} random - Uses endpoints array only, selects random item from array. No fallback support if endpoints array is not provided. 663 | * @param {string} latest-valid-block-height - Uses the endpoint with the latest valid block height. 664 | */ 665 | export type Mode = 'single' | 'first' | 'last' | 'round-robin' | 'random' | 'fastest' | 'highest-slot' | 'latest-valid-block-height'; -------------------------------------------------------------------------------- /package/src/modules/Disperse.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Transaction } from '@solana/web3.js'; 2 | import { ILogger } from '../interfaces/ILogger'; 3 | import { ITransfer } from '../interfaces/ITransfer'; 4 | import { Logger } from './Logger'; 5 | import { TransactionBuilder } from './TransactionBuilder'; 6 | 7 | export type TokenType = 'SOL' | 'SPL'; 8 | 9 | export class Disperse { 10 | private _config: IDisperseConstructor; 11 | private _logger: ILogger = new Logger('@soltoolkit/Disperse'); 12 | 13 | private constructor(values: IDisperseConstructor) { 14 | this._logger.debug(`Disperse constructor called with values: ${JSON.stringify(values, null, 2)}`); 15 | this._config = values; 16 | } 17 | 18 | public static create(values: IDisperseConstructor): Disperse { 19 | return new Disperse(values); 20 | } 21 | 22 | public async generateTransactions(): Promise { 23 | const transactions: Transaction[] = []; 24 | const { tokenType, transfers, sender, fixedAmount, recipients } = this._config; 25 | switch (tokenType) { 26 | case 'SOL': 27 | { 28 | // bundle 18 ixs per tx 29 | let txBuilder = TransactionBuilder.create(); 30 | if (fixedAmount) { 31 | if (recipients === undefined) { 32 | throw new Error('recipients must be defined if fixedAmount is true'); 33 | } else { 34 | for (let x = 0; x < recipients.length; x++) { 35 | // add ix 36 | txBuilder = txBuilder.addSolTransferIx({ 37 | from: sender, 38 | to: new PublicKey(recipients[x]), 39 | amountLamports: fixedAmount 40 | }); 41 | // check if tx is full 42 | if (x % 18 === 0 || x === recipients.length-1) { 43 | txBuilder = txBuilder.addMemoIx({ 44 | memo: "gm! Testing SolToolkit's Disperse module build dispersing SOL 👀", 45 | signer: sender, 46 | }) 47 | this._logger.debug(`Creating new transaction for SOL transfer ${x}`); 48 | transactions.push(txBuilder.build()); 49 | txBuilder = txBuilder.reset(); 50 | } 51 | } 52 | } 53 | 54 | } else { 55 | if (transfers === undefined) { 56 | throw new Error('transfers must be defined if fixedAmount is false'); 57 | } 58 | 59 | for (var x = 0; x < transfers.length; x++) { 60 | this._logger.debug(`Adding SOL ix ${x} to existing transaction`); 61 | 62 | txBuilder = txBuilder.addSolTransferIx({ 63 | from: sender, 64 | to: new PublicKey(transfers[x].recipient), 65 | amountLamports: transfers[x].amount 66 | }); 67 | 68 | if (x % 18 === 0 || x === transfers.length-1) { 69 | txBuilder = txBuilder.addMemoIx({ 70 | memo: "gm! Testing SolToolkit with the Disperse module 👀", 71 | signer: sender, 72 | }) 73 | this._logger.debug(`Creating new transaction for SOL transfer ${x}`); 74 | transactions.push(txBuilder.build()); 75 | txBuilder = txBuilder.reset(); 76 | } 77 | } 78 | } 79 | } 80 | break; 81 | case 'SPL': 82 | throw new Error('SPL token type not yet implemented'); 83 | default: 84 | throw new Error(`Invalid token type: ${tokenType}`); 85 | } 86 | return transactions; 87 | } 88 | } 89 | 90 | export interface IDisperseConstructor { 91 | tokenType: TokenType; 92 | mint?: PublicKey; 93 | maximumAmount?: number; 94 | transfers?: ITransfer[]; 95 | recipients?: PublicKey[]; 96 | fixedAmount?: number; 97 | sender: PublicKey; 98 | } 99 | -------------------------------------------------------------------------------- /package/src/modules/Logger.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../interfaces/ILogger'; 2 | 3 | export class Logger implements ILogger { 4 | private module: string; 5 | public constructor(module: string) { 6 | this.module = module; 7 | } 8 | public debug(...args: any[]): void { 9 | console.debug(`${new Date().toISOString()} - [${this.module}] - DEBUG -`, ...args); 10 | } 11 | public info(...args: any[]): void { 12 | console.info(`${new Date().toISOString()} - [${this.module}] - INFO -`, ...args); 13 | } 14 | public warn(...args: any[]): void { 15 | console.warn(`${new Date().toISOString()} - [${this.module}] - WARN -`, ...args); 16 | } 17 | public error(...args: any[]): void { 18 | console.error(`${new Date().toISOString()} - [${this.module}] - ERROR -`, ...args); 19 | } 20 | public makeError(...args: any[]): Error { 21 | return new Error(...args); 22 | } 23 | } -------------------------------------------------------------------------------- /package/src/modules/SNSDomainResolver.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import fetch from "node-fetch"; 3 | interface DomainLookupResponse { 4 | s: string; 5 | result: { 6 | key: string; 7 | domain: string; 8 | }[]; 9 | } 10 | 11 | /** 12 | * A class for resolving .sol domains to public keys. 13 | */ 14 | export default class SNSDomainResolver { 15 | /** 16 | * Get the first .sol domain associated with an address. 17 | * @param address Public key or address string. 18 | * @returns The domain as a string or null if not found. 19 | */ 20 | static async getDomainFromAddress(address: string | PublicKey): Promise { 21 | const addressString = typeof address === 'string' ? address : address.toBase58(); 22 | const url = `https://sns-sdk-proxy.bonfida.workers.dev/domains/${addressString}`; 23 | try { 24 | const response = await fetch(url); 25 | const json = await response.json() as DomainLookupResponse; 26 | return json.result.sort((a, b) => a.key.localeCompare(b.key))[0].domain; 27 | } catch (e) { 28 | return null; 29 | } 30 | } 31 | /** 32 | * Get all .sol domains associated with an address. 33 | * @param address Public key or address string. 34 | * @returns The domains as an array of strings or null if not found. 35 | */ 36 | static async getDomainsFromAddress(address: string | PublicKey): Promise { 37 | const addressString = typeof address === 'string' ? address : address.toBase58(); 38 | const url = `https://sns-sdk-proxy.bonfida.workers.dev/domains/${addressString}`; 39 | try { 40 | const response = await fetch(url); 41 | const json = await response.json() as DomainLookupResponse; 42 | return json.result.sort((a, b) => a.key.localeCompare(b.key)).map(r => r.domain); 43 | } catch (e) { 44 | return null; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /package/src/modules/SingleTransactionWrapper.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, Connection, PublicKey, ConnectionConfig, Commitment, Signer, VersionedTransaction } from '@solana/web3.js'; 2 | import { IWallet } from '../interfaces/IWallet'; 3 | import { ConnectionManager } from './ConnectionManager'; 4 | 5 | /** 6 | * Represents a wrapper class for a single transaction. 7 | */ 8 | export class SingleTransactionWrapper { 9 | private _transaction: Transaction | VersionedTransaction | Buffer | undefined; 10 | private _connections: Connection[] = []; 11 | private _shouldAddBlockhash = true; 12 | private _shouldAddFeePayer = true; 13 | private _shouldSign = true; 14 | private _shouldConfirm = true; 15 | 16 | private constructor() { } 17 | public create() { return new SingleTransactionWrapper(); } 18 | public setTransaction(transaction: Transaction | VersionedTransaction | Buffer) { 19 | this._transaction = transaction; 20 | return this; 21 | } 22 | public setConnections(connections: Connection[]) { 23 | this._connections = connections; 24 | return this; 25 | } 26 | public addConnection(connection: Connection | ConnectionManager | string, config?: ConnectionConfig | Commitment | undefined) { 27 | if (connection instanceof Connection) { 28 | this._connections.push(connection); 29 | } else if (connection instanceof ConnectionManager) { 30 | this._connections.push(connection.connSync({})); 31 | } else if (typeof connection === 'string') { 32 | this._connections.push(new Connection(connection, config)); 33 | } else { 34 | throw new Error('Invalid connection'); 35 | } 36 | return this; 37 | } 38 | public setShouldAddBlockhash(shouldAddBlockhash: boolean) { 39 | this._shouldAddBlockhash = shouldAddBlockhash; 40 | return this; 41 | } 42 | public setShouldAddFeePayer(shouldAddFeePayer: boolean) { 43 | this._shouldAddFeePayer = shouldAddFeePayer; 44 | return this; 45 | } 46 | public setShouldSign(shouldSign: boolean) { 47 | this._shouldSign = shouldSign; 48 | return this; 49 | } 50 | public setShouldConfirm(shouldConfirm: boolean) { 51 | this._shouldConfirm = shouldConfirm; 52 | return this; 53 | } 54 | public setBlockhash(blockhash: string) { 55 | if (this._transaction instanceof Transaction) { 56 | this._transaction.recentBlockhash = blockhash; 57 | } else if (this._transaction instanceof VersionedTransaction) { 58 | this._transaction.message.recentBlockhash = blockhash; 59 | } else if (this._transaction instanceof Buffer) { 60 | throw new Error('Cannot set blockhash for already serialized transaction'); 61 | } else { 62 | throw new Error('Invalid transaction type'); 63 | } 64 | return this; 65 | } 66 | public setFeePayer(feePayer: PublicKey) { 67 | if (this._transaction instanceof Transaction) { 68 | this._transaction.feePayer = feePayer; 69 | } else if (this._transaction instanceof VersionedTransaction || this._transaction instanceof Buffer) { 70 | throw new Error('Cannot set fee payer for VersionedTransaction or serialized transaction'); 71 | } else { 72 | throw new Error('Invalid transaction type'); 73 | } 74 | return this; 75 | } 76 | public async send({ 77 | wallet, signer, signers, confirmationCommitment, blockhashOverride, feePayerOverride, shouldConfirmOverride, shouldRaceSend, skipPreflight 78 | }: { 79 | wallet?: IWallet; 80 | signer?: Signer; 81 | signers?: Signer[]; 82 | confirmationCommitment?: Commitment; // default is 'max' 83 | blockhashOverride?: string; 84 | feePayerOverride?: PublicKey; 85 | shouldConfirmOverride?: boolean; 86 | shouldRaceSend?: boolean; 87 | skipPreflight?: boolean; 88 | }) { 89 | // validate transaction has been set/has instructions 90 | if (this._transaction === undefined) { 91 | throw new Error('Transaction is undefined'); 92 | } 93 | if (this._transaction instanceof Transaction && this._transaction.instructions.length === 0) { 94 | throw new Error('Transaction has no instructions'); 95 | } 96 | if (this._transaction instanceof VersionedTransaction && this._transaction.message.compiledInstructions.length === 0) { 97 | throw new Error('Transaction has no instructions'); 98 | } 99 | 100 | // validate at least one connection has been set 101 | if (this._connections.length === 0) { 102 | throw new Error('No connections provided'); 103 | } 104 | 105 | // get blockhash if needed 106 | let blockhash: string | undefined; 107 | if (this._shouldAddBlockhash || blockhashOverride !== undefined) { 108 | blockhash = blockhashOverride || (await this._connections[0].getLatestBlockhash({ 109 | commitment: confirmationCommitment || 'max' 110 | })).blockhash; 111 | } 112 | 113 | // add blockhash to transaction 114 | if (this._transaction instanceof Transaction && this._shouldAddBlockhash) { 115 | this._transaction.recentBlockhash = blockhash!; 116 | } else if (this._transaction instanceof VersionedTransaction && this._shouldAddBlockhash) { 117 | this._transaction.message.recentBlockhash = blockhash!; 118 | } 119 | 120 | // add fee payer to transaction if needed 121 | if (this._transaction instanceof Transaction && this._shouldAddFeePayer && feePayerOverride !== undefined) { 122 | this._transaction.feePayer = feePayerOverride; 123 | } 124 | 125 | // sign transaction if needed 126 | if (this._shouldSign && (wallet || signer || signers) && (this._transaction instanceof Transaction || this._transaction instanceof VersionedTransaction)) { 127 | if (wallet && this._transaction instanceof Transaction) { 128 | this._transaction = await wallet.signTransaction(this._transaction); 129 | } else if (signer) { 130 | if (this._transaction instanceof Transaction) { 131 | this._transaction.sign(signer); 132 | } else if (this._transaction instanceof VersionedTransaction) { 133 | this._transaction.sign([signer]); 134 | } 135 | } else if (signers) { 136 | for (const s of signers) { 137 | if (this._transaction instanceof Transaction) { 138 | this._transaction.sign(s); 139 | } else if (this._transaction instanceof VersionedTransaction) { 140 | this._transaction.sign([s]); 141 | } 142 | } 143 | } 144 | } 145 | 146 | // send transaction 147 | let signatures: string[] = []; 148 | if (shouldRaceSend) { 149 | await Promise.race(this._connections.map(async (conn) => { 150 | let signature = await this.sendTransaction(skipPreflight, conn); 151 | signatures.push(signature); 152 | })); 153 | } else { 154 | let signature = await this.sendTransaction(skipPreflight, this._connections[0]); 155 | signatures.push(signature); 156 | } 157 | 158 | // confirm transaction if needed 159 | if (this._shouldConfirm && shouldConfirmOverride && signatures.length > 0 && shouldRaceSend === false) { 160 | await Promise.all(signatures.map(async (sig) => { 161 | return await this._connections[0].confirmTransaction(sig, confirmationCommitment || 'max'); 162 | })); 163 | } else if (this._shouldConfirm && shouldConfirmOverride && signatures.length > 0 && shouldRaceSend) { 164 | await Promise.all(this._connections.map(async (conn) => { 165 | return await Promise.all(signatures.map(async (sig) => { 166 | return await conn.confirmTransaction(sig, confirmationCommitment || 'max'); 167 | })); 168 | })); 169 | } 170 | 171 | return signatures; 172 | } 173 | private async sendTransaction(skipPreflight: boolean | undefined, connection: Connection) { 174 | connection = connection || this._connections[0]; 175 | let signature: string | undefined; 176 | if (this._transaction instanceof Transaction) { 177 | signature = await this._connections[0].sendRawTransaction(this._transaction.serialize(), { 178 | skipPreflight: skipPreflight || false 179 | }); 180 | } else if (this._transaction instanceof VersionedTransaction) { 181 | signature = await this._connections[0].sendTransaction(this._transaction, { skipPreflight: skipPreflight || false }); 182 | } else if (this._transaction instanceof Buffer) { 183 | signature = await this._connections[0].sendRawTransaction(this._transaction, { 184 | skipPreflight: skipPreflight || false 185 | }); 186 | } else { 187 | throw new Error('Invalid transaction type'); 188 | } 189 | return signature; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /package/src/modules/TransactionBuilder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAssociatedTokenAccountInstruction, 3 | createTransferInstruction, 4 | getAssociatedTokenAddressSync 5 | } from '@solana/spl-token'; 6 | import { 7 | Transaction, 8 | TransactionInstruction, 9 | Connection, 10 | PublicKey, 11 | SystemProgram, 12 | Signer, 13 | ComputeBudgetProgram 14 | } from '@solana/web3.js'; 15 | import { ILogger } from '../interfaces/ILogger'; 16 | import { ConnectionManager } from './ConnectionManager'; 17 | import { Logger } from './Logger'; 18 | 19 | export const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'); 20 | 21 | export class TransactionBuilder { 22 | private _transaction: Transaction; 23 | private _instructions: TransactionInstruction[]; 24 | private _logger: ILogger = new Logger('@soltoolkit/TransactionBuilder'); 25 | 26 | private constructor() { 27 | this._transaction = new Transaction(); 28 | this._instructions = []; 29 | } 30 | 31 | public static create(): TransactionBuilder { 32 | return new TransactionBuilder(); 33 | } 34 | 35 | public async addCreateTokenAccountIx({ 36 | connectionOrConnectionManager, 37 | mint, 38 | owner, 39 | payer 40 | }: { 41 | connectionOrConnectionManager: Connection | ConnectionManager; 42 | mint: PublicKey; 43 | owner: PublicKey; 44 | payer: PublicKey; 45 | }): Promise { 46 | var connection: Connection; 47 | if (connectionOrConnectionManager instanceof Connection) { 48 | connection = connectionOrConnectionManager; 49 | } else if (connectionOrConnectionManager instanceof ConnectionManager) { 50 | connection = connectionOrConnectionManager._connection; 51 | } else { 52 | throw new Error('Invalid connectionOrConnectionManager'); 53 | } 54 | 55 | const associatedAddr = getAssociatedTokenAddressSync(mint, owner); 56 | const accInfo = await connection.getAccountInfo(associatedAddr); 57 | if (accInfo === null) { 58 | const ix = createAssociatedTokenAccountInstruction(payer, associatedAddr, owner, mint); 59 | this.addIx(ix); 60 | } else { 61 | this._logger.info(`Token account already exists: ${associatedAddr.toBase58()}`); 62 | } 63 | return this; 64 | } 65 | 66 | public addSolTransferIx({ from, to, amountLamports }: { from: PublicKey; to: PublicKey; amountLamports: number }) { 67 | const ix = SystemProgram.transfer({ 68 | fromPubkey: from, 69 | toPubkey: to, 70 | lamports: amountLamports 71 | }); 72 | this.addIx(ix); 73 | return this; 74 | } 75 | 76 | public addSplTransferIx({ 77 | fromTokenAccount, 78 | toTokenAccount, 79 | rawAmount, 80 | owner, 81 | additionalSigners 82 | }: { 83 | fromTokenAccount: PublicKey; 84 | toTokenAccount: PublicKey; 85 | rawAmount: number; 86 | owner: PublicKey; 87 | additionalSigners?: Signer[]; 88 | }) { 89 | const ix = createTransferInstruction(fromTokenAccount, toTokenAccount, owner, rawAmount, additionalSigners); 90 | this.addIx(ix); 91 | return this; 92 | } 93 | 94 | public addSplTransferIxByOwners({ 95 | mint, 96 | fromOwner, 97 | toOwner, 98 | rawAmount, 99 | additionalSigners 100 | }: { 101 | mint: PublicKey; 102 | fromOwner: PublicKey; 103 | toOwner: PublicKey; 104 | rawAmount: number; 105 | additionalSigners?: Signer[]; 106 | }) { 107 | // get associated token accounts 108 | const fromTokenAccount = getAssociatedTokenAddressSync(mint, fromOwner); 109 | const toTokenAccount = getAssociatedTokenAddressSync(mint, toOwner); 110 | // create transfer instruction 111 | const ix = createTransferInstruction( 112 | fromTokenAccount, 113 | toTokenAccount, 114 | fromOwner, 115 | rawAmount, 116 | additionalSigners 117 | ); 118 | // add instruction to the list 119 | this.addIx(ix); 120 | return this; 121 | } 122 | 123 | public addMemoIx({ memo, signer }: { memo: string; signer: PublicKey }) { 124 | const ix = new TransactionInstruction({ 125 | keys: [{ pubkey: signer, isSigner: true, isWritable: true }], 126 | data: Buffer.from(memo), 127 | programId: new PublicKey(MEMO_PROGRAM_ID) 128 | }); 129 | this.addIx(ix); 130 | return this; 131 | } 132 | 133 | public addComputeBudgetIx({ 134 | units, 135 | priceInMicroLamports 136 | }: { 137 | units: number; 138 | priceInMicroLamports: number; 139 | }) { 140 | const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ 141 | units 142 | }); 143 | const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({ 144 | microLamports: priceInMicroLamports 145 | }); 146 | this._instructions.unshift(modifyComputeUnits, addPriorityFee); 147 | return this; 148 | } 149 | 150 | 151 | 152 | public addIx(instruction: TransactionInstruction | TransactionInstruction[]): TransactionBuilder { 153 | this._instructions = this._instructions.concat(instruction); 154 | this.logNumberOfIxs(); 155 | return this; 156 | } 157 | 158 | public reset(): TransactionBuilder { 159 | this._transaction = new Transaction(); 160 | this._instructions = []; 161 | this._logger.warn('resetting builder'); 162 | return this; 163 | } 164 | 165 | public build(): Transaction { 166 | this.logNumberOfIxs(); 167 | this._transaction.instructions = this._instructions; 168 | return this._transaction; 169 | } 170 | 171 | private logNumberOfIxs = () => this._logger.debug(`instruction count: ${this._instructions.length}`); 172 | } 173 | -------------------------------------------------------------------------------- /package/src/modules/TransactionHelper.ts: -------------------------------------------------------------------------------- 1 | import { ComputeBudgetProgram, Connection, PublicKey, Signer, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js"; 2 | import { ConnectionManager } from "./ConnectionManager"; 3 | import { createAssociatedTokenAccountInstruction, createTransferInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token"; 4 | import { MEMO_PROGRAM_ID } from "./TransactionBuilder"; 5 | 6 | /** 7 | * Helper class for building Solana transactions. 8 | */ 9 | export class TransactionHelper { 10 | /** 11 | * Sourced from: https://solana.stackexchange.com/questions/5628/is-there-a-way-to-estimate-the-transaction-size 12 | * @param tx a solana transaction 13 | * @param feePayer the publicKey of the signer 14 | * @returns size in bytes of the transaction 15 | */ 16 | public static getTxSize(tx: Transaction, feePayer: PublicKey): number { 17 | const feePayerPk = [feePayer.toBase58()]; 18 | 19 | const signers = new Set(feePayerPk); 20 | const accounts = new Set(feePayerPk); 21 | 22 | const ixsSize = tx.instructions.reduce((acc, ix) => { 23 | ix.keys.forEach(({ pubkey, isSigner }) => { 24 | const pk = pubkey.toBase58(); 25 | if (isSigner) signers.add(pk); 26 | accounts.add(pk); 27 | }); 28 | 29 | accounts.add(ix.programId.toBase58()); 30 | 31 | const nIndexes = ix.keys.length; 32 | const opaqueData = ix.data.length; 33 | 34 | return ( 35 | acc + 36 | 1 + // PID index 37 | this.compactArraySize(nIndexes, 1) + 38 | this.compactArraySize(opaqueData, 1) 39 | ); 40 | }, 0); 41 | 42 | return ( 43 | this.compactArraySize(signers.size, 64) + // signatures 44 | 3 + // header 45 | this.compactArraySize(accounts.size, 32) + // accounts 46 | 32 + // blockhash 47 | this.compactHeader(tx.instructions.length) + // instructions 48 | ixsSize 49 | ); 50 | } 51 | 52 | // COMPACT ARRAY 53 | static LOW_VALUE = 127; // 0x7f 54 | static HIGH_VALUE = 16383; // 0x3fff 55 | 56 | /** 57 | * Compact u16 array header size 58 | * @param n elements in the compact array 59 | * @returns size in bytes of array header 60 | */ 61 | static compactHeader = (n: number) => (n <= this.LOW_VALUE ? 1 : n <= this.HIGH_VALUE ? 2 : 3); 62 | 63 | /** 64 | * Compact u16 array size 65 | * @param n elements in the compact array 66 | * @param size bytes per each element 67 | * @returns size in bytes of array 68 | */ 69 | static compactArraySize = (n: number, size: number) => this.compactHeader(n) + n * size; 70 | 71 | /** 72 | * Creates a token account creation instruction if account does not exist already. 73 | * @param connectionOrConnectionManager The connection or connection manager. 74 | * @param mint The mint public key. 75 | * @param owner The owner public key. 76 | * @param payer The payer public key. 77 | * @returns A promise that resolves to a TransactionInstruction or null. 78 | */ 79 | public static async createTokenAccountIx({ 80 | connectionOrConnectionManager, 81 | mint, 82 | owner, 83 | payer 84 | }: { 85 | connectionOrConnectionManager: Connection | ConnectionManager; 86 | mint: PublicKey; 87 | owner: PublicKey; 88 | payer: PublicKey; 89 | }): Promise { 90 | var connection: Connection; 91 | if (connectionOrConnectionManager instanceof Connection) { 92 | connection = connectionOrConnectionManager; 93 | } else if (connectionOrConnectionManager instanceof ConnectionManager) { 94 | connection = connectionOrConnectionManager._connection; 95 | } else { 96 | throw new Error('Invalid connectionOrConnectionManager'); 97 | } 98 | 99 | const doesExist = await this.doesTokenAccountExist({ connectionOrConnectionManager: connection, mint, owner }); 100 | if (!doesExist) { 101 | const associatedAddr = getAssociatedTokenAddressSync(mint, owner); 102 | const ix = createAssociatedTokenAccountInstruction(payer, associatedAddr, owner, mint); 103 | return ix; 104 | } else { 105 | return null; 106 | } 107 | } 108 | 109 | /** 110 | * Creates a Solana transfer instruction. 111 | * @param from The public key of the sender. 112 | * @param to The public key of the recipient. 113 | * @param amountLamports The amount of lamports to transfer. 114 | * @returns The transfer instruction. 115 | */ 116 | public static createSolTransferIx({ 117 | from, 118 | to, 119 | amountLamports 120 | }: { 121 | from: PublicKey; 122 | to: PublicKey; 123 | amountLamports: number; 124 | }): TransactionInstruction { 125 | return SystemProgram.transfer({ 126 | fromPubkey: from, 127 | toPubkey: to, 128 | lamports: amountLamports 129 | }); 130 | } 131 | 132 | /** 133 | * Creates a transaction instruction for transferring SPL tokens. 134 | * 135 | * @param fromTokenAccount The public key of the token account to transfer from. 136 | * @param toTokenAccount The public key of the token account to transfer to. 137 | * @param rawAmount The amount of tokens to transfer. 138 | * @param owner The public key of the account that owns the token account. 139 | * @param additionalSigners (Optional) An array of additional signers for the transaction. 140 | * @returns The transfer instruction. 141 | */ 142 | public static createSplTransferIx({ 143 | fromTokenAccount, 144 | toTokenAccount, 145 | rawAmount, 146 | owner, 147 | additionalSigners 148 | }: { 149 | fromTokenAccount: PublicKey; 150 | toTokenAccount: PublicKey; 151 | rawAmount: number; 152 | owner: PublicKey; 153 | additionalSigners?: Signer[]; 154 | }): TransactionInstruction { 155 | return createTransferInstruction(fromTokenAccount, toTokenAccount, owner, rawAmount, additionalSigners); 156 | } 157 | 158 | /** 159 | * Creates a memo instruction. 160 | * 161 | * @param memo The memo to include in the instruction. 162 | * @param signer The public key of the signer. 163 | * @returns The memo instruction. 164 | */ 165 | public static createMemoIx({ 166 | memo, 167 | signer 168 | }: { 169 | memo: string; 170 | signer: PublicKey; 171 | }): TransactionInstruction { 172 | return new TransactionInstruction({ 173 | keys: [{ pubkey: signer, isSigner: true, isWritable: true }], 174 | data: Buffer.from(memo), 175 | programId: new PublicKey(MEMO_PROGRAM_ID) 176 | }); 177 | } 178 | 179 | /** 180 | * Creates a compute budget instruction. 181 | * 182 | * @param units The number of compute units to request. 183 | * @returns The compute budget instruction. 184 | */ 185 | public static addComputeBudgetIx({ 186 | units 187 | }: { 188 | units: number; 189 | }): TransactionInstruction { 190 | const ix = ComputeBudgetProgram.requestUnits({ 191 | units, 192 | additionalFee: 0 193 | }); 194 | return ix; 195 | } 196 | 197 | /** 198 | * Checks if a token account exists. Returns true if it does, false if it does not. 199 | * @param connectionOrConnectionManager The connection or connection manager. 200 | * @param mint The token mint as a public key. 201 | * @param owner The owner as a public key. 202 | * @returns A promise that resolves to a boolean. 203 | */ 204 | public static async doesTokenAccountExist({ 205 | connectionOrConnectionManager, 206 | mint, 207 | owner 208 | }: { 209 | connectionOrConnectionManager: Connection | ConnectionManager; 210 | mint: PublicKey; 211 | owner: PublicKey; 212 | }): Promise { 213 | var connection: Connection; 214 | if (connectionOrConnectionManager instanceof Connection) { 215 | connection = connectionOrConnectionManager; 216 | } else if (connectionOrConnectionManager instanceof ConnectionManager) { 217 | connection = connectionOrConnectionManager._connection; 218 | } else { 219 | throw new Error('Invalid connectionOrConnectionManager'); 220 | } 221 | 222 | const associatedAddr = getAssociatedTokenAddressSync(mint, owner); 223 | const accInfo = await connection.getAccountInfo(associatedAddr); 224 | return accInfo !== null; 225 | } 226 | 227 | public static getAssociatedTokenAddressSync = getAssociatedTokenAddressSync; 228 | } -------------------------------------------------------------------------------- /package/src/modules/TransactionWrapper.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, Connection, PublicKey, ConnectionConfig, Commitment, Signer, SendOptions } from '@solana/web3.js'; 2 | import { ILogger } from '../interfaces/ILogger'; 3 | import { IWallet } from '../interfaces/IWallet'; 4 | import { ConnectionManager } from './ConnectionManager'; 5 | import { Logger } from './Logger'; 6 | import bs58 from 'bs58'; 7 | import fetch from 'node-fetch'; 8 | 9 | /** 10 | * TransactionWrapper is a utility class that simplifies the process of creating, signing, sending, and confirming transactions. 11 | */ 12 | export class TransactionWrapper { 13 | private _transactions: Transaction[]; 14 | private _connection: Connection; 15 | private _logger: ILogger = new Logger('@soltoolkit/TransactionWrapper'); 16 | private _feePayer?: PublicKey; 17 | 18 | private constructor(connection: Connection, transaction?: Transaction | Transaction[], feePayer?: PublicKey) { 19 | this._transactions = transaction ? (Array.isArray(transaction) ? transaction : [transaction]) : []; 20 | this._connection = connection; 21 | this._feePayer = feePayer; 22 | } 23 | 24 | public static create({ 25 | transaction, 26 | transactions, 27 | rpcEndpoint, 28 | connection, 29 | connectionManager, 30 | config, 31 | changeConn = false 32 | }: { 33 | transaction?: Transaction; 34 | transactions?: Transaction[]; 35 | rpcEndpoint?: string; 36 | connection?: Connection; 37 | connectionManager?: ConnectionManager; 38 | config?: ConnectionConfig; 39 | changeConn?: boolean; 40 | }): TransactionWrapper { 41 | var conn: Connection; 42 | 43 | if (connection) { 44 | conn = connection; 45 | } else if (rpcEndpoint) { 46 | conn = new Connection(rpcEndpoint, config); 47 | } else if (connectionManager) { 48 | conn = connectionManager.connSync({ changeConn }); 49 | } else { 50 | throw new Error('No connection or rpc endpoint provided'); 51 | } 52 | 53 | return new TransactionWrapper(conn, transaction || transactions); 54 | } 55 | 56 | public async sendAndConfirm({ 57 | serialisedTx, 58 | maximumRetries = 5, 59 | commitment = 'max', 60 | skipPreflight = false 61 | }: { 62 | serialisedTx: Uint8Array | Buffer | number[]; 63 | maximumRetries?: number; 64 | commitment?: Commitment; 65 | skipPreflight?: boolean; 66 | }): Promise { 67 | var signature: string | undefined; 68 | var tries = 0; 69 | var isTransactionConfirmed = false; 70 | while ( 71 | tries < maximumRetries && // not exceeded max retries 72 | !isTransactionConfirmed // no confirmation of any signature 73 | ) { 74 | try { 75 | signature = await this.sendTx({ serialisedTx, skipPreflight }); 76 | const result = await this.confirmTx({ signature, commitment }); 77 | if (result.value.err !== null) { 78 | throw new Error(`RPC failure: ${JSON.stringify(result.value.err)}`); 79 | } 80 | this._logger.debug(result); 81 | isTransactionConfirmed = true; 82 | } catch (e: any) { 83 | if (e.message.includes('RPC failure')) { 84 | throw e; 85 | } else { 86 | this._logger.warn('Transaction failed, retrying...', e); 87 | tries++; 88 | } 89 | } 90 | } 91 | 92 | if (signature === undefined || !isTransactionConfirmed) { 93 | throw this._logger.makeError(`Transaction failed after ${tries} tries`); 94 | } 95 | 96 | return signature; 97 | } 98 | 99 | public async addBlockhashAndFeePayer(feePayer?: PublicKey) { 100 | const latestBlockhash = await this._connection.getLatestBlockhash(); 101 | for (const transaction of this._transactions) { 102 | transaction.recentBlockhash = latestBlockhash.blockhash; 103 | transaction.feePayer = feePayer || this._feePayer; 104 | 105 | if (transaction.feePayer === undefined) { 106 | throw new Error('Fee payer must be defined'); 107 | } 108 | 109 | this._logger.debug('blockhash:', transaction.recentBlockhash); 110 | this._logger.debug('fee payer:', transaction.feePayer.toBase58()); 111 | } 112 | return this; 113 | } 114 | 115 | public async sign({ 116 | wallet, 117 | signers, 118 | txs 119 | }: { 120 | wallet?: IWallet; 121 | signers?: Signer[]; 122 | txs?: Transaction[]; 123 | }): Promise { 124 | if (!wallet && !signers) { 125 | throw new Error('No wallet or signers provided'); 126 | } 127 | 128 | if (txs === undefined) { 129 | txs = this._transactions; 130 | } 131 | 132 | if (wallet) { 133 | var signedTx = await wallet.signAllTransactions(txs); 134 | return signedTx!; 135 | } else if (signers) { 136 | for (const signer of signers) { 137 | for (const transaction of txs) { 138 | transaction.sign(signer); 139 | } 140 | } 141 | return txs; 142 | } else { 143 | throw new Error('Wallet or Signer must be provided'); 144 | } 145 | } 146 | 147 | public async sendTx({ 148 | serialisedTx, 149 | skipPreflight = false 150 | }: { 151 | serialisedTx: Uint8Array | Buffer | number[]; 152 | skipPreflight?: boolean; 153 | }) { 154 | var sig = await this._connection.sendRawTransaction(serialisedTx, { 155 | skipPreflight 156 | }); 157 | return sig; 158 | } 159 | 160 | public async sendTxUsingJito({ 161 | serializedTx, 162 | region = 'mainnet' 163 | }: { 164 | serializedTx: Uint8Array | Buffer | number[]; 165 | region: JitoRegion; 166 | }) { 167 | return await sendTxUsingJito({ serializedTx, region }); 168 | } 169 | 170 | public async confirmTx({ signature, commitment = 'max' }: { signature: string; commitment?: Commitment }) { 171 | const latestBlockHash = await this._connection.getLatestBlockhash(commitment); 172 | 173 | return await this._connection.confirmTransaction( 174 | { 175 | signature: signature, 176 | blockhash: latestBlockHash.blockhash, 177 | lastValidBlockHeight: latestBlockHash.lastValidBlockHeight 178 | }, 179 | commitment 180 | ); 181 | } 182 | 183 | public static async confirmTx({ 184 | connection, 185 | connectionManager, 186 | signature, 187 | commitment = 'max', 188 | changeConn = false, 189 | airdrop 190 | }: { 191 | connection?: Connection; 192 | connectionManager?: ConnectionManager; 193 | signature: string; 194 | commitment?: Commitment; 195 | changeConn?: boolean; 196 | airdrop?: boolean; 197 | }) { 198 | // if connection is not provided, use connection manager 199 | if (connection === undefined && connectionManager !== undefined) { 200 | connection = connectionManager.connSync({ changeConn, airdrop }); 201 | } else if (connection === undefined && connectionManager === undefined) { 202 | throw new Error('No connection or connection manager provided'); 203 | } 204 | 205 | if (connection === undefined) { 206 | throw new Error('Connection is undefined'); 207 | } 208 | 209 | const latestBlockHash = await connection.getLatestBlockhash(commitment); 210 | 211 | return await connection.confirmTransaction( 212 | { 213 | signature: signature, 214 | blockhash: latestBlockHash.blockhash, 215 | lastValidBlockHeight: latestBlockHash.lastValidBlockHeight 216 | }, 217 | commitment 218 | ); 219 | } 220 | } 221 | 222 | export type JitoRegion = 'mainnet' | 'amsterdam' | 'frankfurt' | 'ny' | 'tokyo'; 223 | export const JitoEndpoints = { 224 | mainnet: 'https://mainnet.block-engine.jito.wtf/api/v1/', 225 | amsterdam: 'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/', 226 | frankfurt: 'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/', 227 | ny: 'https://ny.mainnet.block-engine.jito.wtf/api/v1/', 228 | tokyo: 'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/' 229 | }; 230 | interface BundleStatusResult { 231 | bundle_id: string; 232 | transactions: string[]; 233 | slot: number; 234 | confirmation_status: string; 235 | err?: any; 236 | } 237 | export function getJitoEndpoint(region: JitoRegion) { 238 | return JitoEndpoints[region]; 239 | } 240 | /** 241 | * Send a transaction using Jito. This only supports sending a single transaction on mainnet only. 242 | * See https://jito-labs.gitbook.io/mev/searcher-resources/json-rpc-api-reference/transactions-endpoint/sendtransaction. 243 | * @param args.serialisedTx - A single transaction to be sent, in serialised form 244 | * @param args.region - The region of the Jito endpoint to use 245 | * @returns The signature of the transaction 246 | */ 247 | export async function sendTxUsingJito({ 248 | serializedTx, 249 | region = 'mainnet' 250 | }: { 251 | serializedTx: Uint8Array | Buffer | number[]; 252 | region?: JitoRegion; 253 | }): Promise { 254 | let rpcEndpoint = getJitoEndpoint(region); 255 | let encodedTx = bs58.encode(serializedTx); 256 | let payload = { 257 | jsonrpc: '2.0', 258 | id: 1, 259 | method: 'sendTransaction', 260 | params: [encodedTx] 261 | }; 262 | let res = await fetch(`${rpcEndpoint}/transactions?bundleOnly=true`, { 263 | method: 'POST', 264 | body: JSON.stringify(payload), 265 | headers: { 'Content-Type': 'application/json' } 266 | }); 267 | let json = await res.json(); 268 | if (json.error) { 269 | throw new Error(json.error.message); 270 | } 271 | return json.result; 272 | } 273 | /** 274 | * Send a bundle of transactions using Jito. 275 | * @param param0.signedTxs - An array of signed transactions 276 | * @param param0.region - The region of the Jito endpoint to use. Defaults to mainnet. 277 | * @returns A bundle ID, used to identify the bundle. This is the SHA-256 hash of the bundle's transaction signatures. 278 | */ 279 | export async function sendTransactionsAsBundleUsingJito({ 280 | signedTxs, 281 | region = 'mainnet' 282 | }: { 283 | signedTxs: Transaction[]; 284 | region?: JitoRegion; 285 | }): Promise { 286 | // Get the endpoint for the region 287 | let rpcEndpoint = getJitoEndpoint(region); 288 | 289 | // Encode the transactions 290 | let encodedTxs = signedTxs.map((tx) => 291 | bs58.encode( 292 | tx.serialize({ 293 | // Skip signature verification 294 | verifySignatures: true 295 | }) 296 | ) 297 | ); 298 | 299 | // Send bundle 300 | let payload = { 301 | jsonrpc: '2.0', 302 | id: 1, 303 | method: 'sendBundle', 304 | params: [encodedTxs] 305 | }; 306 | let res = await fetch(`${rpcEndpoint}/bundles`, { 307 | method: 'POST', 308 | body: JSON.stringify(payload), 309 | headers: { 'Content-Type': 'application/json' } 310 | }); 311 | 312 | // Parse response and return bundle ID 313 | let json = await res.json(); 314 | if (json.error) { 315 | throw new Error(json.error.message); 316 | } 317 | return json.result; 318 | } 319 | /** 320 | * Get the status of a bundle using Jito. 321 | * @param param0.bundleId - The bundle ID to get the status of. 322 | * @returns The status of the bundle, or null if the bundle does not exist. 323 | */ 324 | export async function getJitoBundleStatus({ 325 | bundleId, 326 | region = 'mainnet' 327 | }: { 328 | bundleId: string; 329 | region?: JitoRegion; 330 | }): Promise { 331 | // Get the endpoint for the region 332 | let rpcEndpoint = getJitoEndpoint(region); 333 | 334 | // Send bundle status request 335 | let payload = { 336 | jsonrpc: '2.0', 337 | id: 1, 338 | method: 'getBundleStatuses', 339 | params: [[bundleId]] 340 | }; 341 | let res = await fetch(`${rpcEndpoint}/bundles`, { 342 | method: 'POST', 343 | body: JSON.stringify(payload), 344 | headers: { 'Content-Type': 'application/json' } 345 | }); 346 | 347 | // Parse response 348 | let json = await res.json(); 349 | if (json === null) { 350 | return null; 351 | } 352 | if (json.error) { 353 | throw new Error(json.error.message); 354 | } 355 | return json.result.value as BundleStatusResult[]; 356 | } 357 | -------------------------------------------------------------------------------- /package/src/modules/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Timeout error 3 | */ 4 | export class TimeoutError extends Error { 5 | constructor(timeElapsed: number) { 6 | super(`Timeout of ${timeElapsed}ms exceeded`); 7 | this.name = 'TimeoutError'; 8 | } 9 | } 10 | 11 | /** 12 | * Rejects a promise after a given time. Useful for timeouts in async functions. 13 | * Rejection is a TimeoutError. 14 | * @param time Time in milliseconds 15 | * @returns Promise that rejects after the given time 16 | */ 17 | export function rejectAfter(time: number): Promise { 18 | return new Promise((_, reject) => { 19 | setTimeout(() => reject(new TimeoutError(time)), time); 20 | }); 21 | }; -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "commonjs", /* Specify what module code is generated. */ 5 | "rootDir": "./src", /* Specify the root folder within your source files. */ 6 | "resolveJsonModule": true, /* Enable importing .json files. */ 7 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 8 | "outDir": "./build", /* Specify an output folder for all emitted files. */ 9 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 10 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 11 | "strict": true, /* Enable all strict type-checking options. */ 12 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 13 | } 14 | } 15 | --------------------------------------------------------------------------------