├── .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 |
5 |
6 |
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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------