β½Scale by Solana at Solana
Dispersing SOL to 10,000 users in <120 seconds
import { Commitment, Keypair, LAMPORTS_PER_SOL, Signer, Transaction, } from "@solana/web3.js"; import { ConnectionManager, Disperse, TransactionBuilder, TransactionWrapper, Logger } from "@solworks/soltoolkit-sdk";
const COMMITMENT: Commitment = "confirmed"; const NO_OF_RECEIVERS = 10_000; const CHUNK_SIZE = 30; const TOTAL_SOL = 10;
const SKIP_AIRDROP = true; const SKIP_SENDING = false; const SKIP_BALANCE_CHECK = true;
// generate keypair for example const sender = Keypair.fromSecretKey( Uint8Array.from([ 36, 50, 153, 146, 147, 239, 210, 72, 199, 68, 75, 220, 42, 139, 105, 61, 148, 117, 55, 75, 23, 144, 30, 206, 138, 255, 51, 206, 102, 239, 73, 28, 240, 73, 69, 190, 238, 27, 112, 36, 151, 255, 182, 64, 13, 173, 94, 115, 111, 45, 2, 154, 250, 93, 100, 44, 251, 111, 229, 34, 193, 249, 199, 238, ]) );
(async () => { const logger = new Logger("example");
// create connection manager const cm = await ConnectionManager.getInstance({ commitment: COMMITMENT, endpoints: [ "https://mango.devnet.rpcpool.com",
// https://docs.solana.com/cluster/rpc-endpoints
// Maximum number of requests per 10 seconds per IP: 100 (10/s)
// Maximum number of requests per 10 seconds per IP for a single RPC: 40 (4/s)
// Maximum concurrent connections per IP: 40
// Maximum connection rate per 10 seconds per IP: 40
// Maximum amount of data per 30 second: 100 MB
// "https://api.devnet.solana.com",
// https://shdw.genesysgo.com/genesysgo/the-genesysgo-rpc-network
// SendTransaction Limit: 10 RPS + 200 Burst
// getProgramAccounts Limit: 15 RPS + 5 burst
// Global Limit on the rest of the calls: 200 RPS
"https://devnet.genesysgo.net",
],
mode: "round-robin",
network: "devnet",
});
if (!SKIP_AIRDROP) { // airdrop 1 sol to new addresses, confirm and send sol to SENDER for (let i = 0; i < Math.ceil((TOTAL_SOL + 1) / 1); i++) { // generate new keypair const keypair = Keypair.generate();
// airdrop sol to the generated address
const airdropSig = await cm
.connSync({ airdrop: true })
.requestAirdrop(keypair.publicKey, LAMPORTS_PER_SOL);
logger.debug("Airdropped 1 SOL to", sender.publicKey.toBase58());
// wait for confirmation
logger.debug("Confirming airdrop transaction...");
await TransactionWrapper.confirmTx({
connectionManager: cm,
changeConn: false,
signature: airdropSig,
commitment: "max",
airdrop: true,
});
logger.debug("Airdrop transaction confirmed");
// send sol to SENDER
const tx = TransactionBuilder.create()
.addSolTransferIx({
from: keypair.publicKey,
to: sender.publicKey,
amountLamports: LAMPORTS_PER_SOL - 5000,
})
.build();
const wrapper = await TransactionWrapper.create({
connectionManager: cm,
changeConn: false,
signer: keypair.publicKey,
transaction: tx,
}).addBlockhashAndFeePayer(keypair.publicKey);
const signedTx = await wrapper.sign({ signer: keypair as Signer });
const sig = await wrapper.sendAndConfirm({
serialisedTx: signedTx.serialize(),
commitment: "max",
});
logger.debug(
"Sent 1 SOL to",
sender.publicKey.toBase58(),
"with signature",
sig
);
await sleep(1000);
}
}
// fetch balance of the generated address logger.debug("Fetching balance of", sender.publicKey.toBase58()); let senderBal = await cm // default value for changeConn = true .connSync({ changeConn: true }) .getBalance(sender.publicKey, COMMITMENT); logger.debug(Sender balance: ${senderBal}
);
// generate receivers logger.debug(Generating ${NO_OF_RECEIVERS} receivers...
); const receivers: Keypair[] = []; for (let i = 0; i < NO_OF_RECEIVERS; i++) { receivers.push(Keypair.generate()); } logger.debug("Receivers generated");
// generate transactions const transfers: { amount: number; recipient: string; }[] = [];
const rentCost = (NO_OF_RECEIVERS+1) * 5_000; const transferAmount = Math.floor( (senderBal - rentCost) / NO_OF_RECEIVERS ); logger.debug(Sending ${transferAmount} to ${NO_OF_RECEIVERS} receivers
); for (let i = 0; i < NO_OF_RECEIVERS; i++) { transfers.push({ amount: transferAmount, recipient: receivers[i].publicKey.toBase58(), }); }
// send transactions if (!SKIP_SENDING) { const transactions = await Disperse.create({ tokenType: "SOL", sender: sender.publicKey, transfers, }).generateTransactions();
const txChunks = chunk(transactions, CHUNK_SIZE);
for (let i = 0; i < txChunks.length; i++) {
logger.debug(`Sending transactions ${i + 1}/${txChunks.length}`);
const txChunk = txChunks[i];
const conn = cm.connSync({ changeConn: true });
await Promise.all(
txChunk.map(async (tx: Transaction, i: number) => {
logger.debug(`Sending transaction ${i + 1}`);
// feed transaction into TransactionWrapper
const wrapper = await TransactionWrapper.create({
connection: conn,
transaction: tx,
signer: sender.publicKey,
}).addBlockhashAndFeePayer(sender.publicKey);
// sign the transaction
logger.debug(`Signing transaction ${i + 1}`);
const signedTx = await wrapper.sign({
signer: sender as Signer,
});
// send and confirm the transaction
logger.debug(`Sending transaction ${i + 1}`);
const transferSig = await wrapper.sendAndConfirm({
serialisedTx: signedTx.serialize(),
commitment: COMMITMENT,
});
logger.debug("Transaction sent:", transferSig.toString());
})
);
await sleep(1_000);
}
}
if (!SKIP_BALANCE_CHECK) { // fetch balance of the generated address logger.debug("Fetching balance of:", sender.publicKey.toBase58()); senderBal = await cm .connSync({ changeConn: true }) .getBalance(sender.publicKey, COMMITMENT); logger.debug(Sender balance: ${senderBal}
);
// split addresses into chunks of CHUNK_SIZE
const chunks = chunk(receivers, CHUNK_SIZE);
const balances: {
balance: number;
address: string;
}[] = [];
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
logger.debug(
`Fetching balances for chunk ${i + 1} with ${chunk.length} addresses`
);
// cycle to new connection to avoid rate limiting
let conn = cm.connSync({ changeConn: true });
// fetch balances
const results = await Promise.all(
chunk.map(async (receiver: Keypair) => {
const balance = await conn.getBalance(receiver.publicKey, COMMITMENT);
logger.debug(
`Balance of ${receiver.publicKey.toBase58()}: ${balance}`
);
return {
balance,
address: receiver.publicKey.toBase58(),
};
})
);
// add results to balances
balances.push(...results);
await sleep(1_000);
}
const totalBalance = balances.reduce((acc, curr) => acc + curr.balance, 0);
const numberWithNoBalance = balances.filter((b) => b.balance === 0).length;
const numberWithBalance = balances.filter((b) => b.balance > 0).length;
logger.debug(`Total amount sent: ${totalBalance}`);
logger.debug(`Number of addresses with no balance: ${numberWithNoBalance}`);
logger.debug(`Number of addresses with balance: ${numberWithBalance}`);
} })();
function chunk(arr: any[], len: number) { var chunks: any[] = [], i = 0, n = arr.length;
while (i < n) { chunks.push(arr.slice(i, (i += len))); } return chunks; }
function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }
Last updated