Skip to content

Instantly share code, notes, and snippets.

@NotoriousPyro
Last active May 15, 2024 19:42
Show Gist options
  • Save NotoriousPyro/1675b1de59ce2496411e4a654bcc2b79 to your computer and use it in GitHub Desktop.
Save NotoriousPyro/1675b1de59ce2496411e4a654bcc2b79 to your computer and use it in GitHub Desktop.
Solana Rewards distribution
import { PublicKey, Transaction, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
import config from "../src/lib/Config";
import BigNumber from "bignumber.js";
import { createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, getAssociatedTokenAddress, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { connection } from "../src/connection";
import { createComputeBudgetInstruction } from "../src/lib/TransactionBuilder";
/**
* Distributes rewards based on a a token amount a user has, e.g. staked or LP tokens.
*
* Caps each person's rewards to the amount they have staked or the total reward available.
*
* Note: not feature complete, requires manual configuration below.
*/
const sendRewards = async () => {
const instruction = new Transaction();
const stakers: { // Collection of all those holding a certain amount of a token you want to distribute rewards based on
publicKey: PublicKey,
staked: BigNumber,
}[] = [
{
publicKey: "9BBRGif6mYsqwrSAFyeHqedEbZyXhyEK3RNfrXR1PoVM",
staked: 536_773,
},
{
publicKey: "9NKLw8pYQUkf3gpBstyA2xurPjWAFrfBarPKW2bBwE2r",
staked: 1_465_398,
},
// {
// publicKey: "7Vjm4ZMMVTB6d14z3SPfnSCyxtUiT4CPLXpwHftg9cVn",
// staked: 1_864_989,
// }
].map(staker => {
return {
publicKey: new PublicKey(staker.publicKey),
staked: new BigNumber(staker.staked),
}
});
const totalStaked = stakers.reduce((acc, curr) => acc.plus(curr.staked), new BigNumber(0));
const multiplier = new BigNumber(totalStaked).dividedBy(4_166_000); // The total rewards that are available based on the total staked
const rewards = [ // The tokens that will be distributed, and the amount that will be distributed based on the staked amount
{
mint: new PublicKey("DARpE2GaVrazeh6mopWXbTT1hV3EbNNvHrJMMqJXUm6i"),
amount: BigNumber.min(4_166_000, new BigNumber(4_166_000).multipliedBy(multiplier)),
decimals: 9,
},
{
mint: new PublicKey("BoZoQQRAmYkr5iJhqo7DChAs7DPDwEZ5cv1vkYC9yzJG"),
amount: BigNumber.min(5_000_000_000_000, new BigNumber(5_000_000_000_000).multipliedBy(multiplier)),
decimals: 5,
}
]
const perStakerRewards: { // The calculated rewards for each staker
publicKey: PublicKey,
rewards: {
mint: PublicKey,
ata: PublicKey,
amount: BigNumber,
decimals: number,
}[]
}[] = await Promise.all(stakers.map(async staker => ({
publicKey: staker.publicKey,
rewards: await Promise.all(rewards.map(async reward => {
return {
mint: reward.mint,
ata: await getAssociatedTokenAddress(reward.mint, staker.publicKey),
amount: reward.amount.multipliedBy(staker.staked).dividedBy(totalStaked).decimalPlaces(0),
decimals: reward.decimals,
}
}))
})));
for (const staker of perStakerRewards) { // Create the instructions to send the rewards and create the associated token accounts
for (const reward of staker.rewards) {
const sourceAta = getAssociatedTokenAddressSync(reward.mint, config.rewardsKeypair.publicKey);
instruction.add(
createAssociatedTokenAccountIdempotentInstruction(config.rewardsKeypair.publicKey, reward.ata, staker.publicKey, reward.mint),
createTransferCheckedInstruction(sourceAta, reward.mint, reward.ata, config.rewardsKeypair.publicKey, BigInt(reward.amount.toString()), reward.decimals)
)
}
}
const bhInfo = await connection.getLatestBlockhashAndContext({ commitment: "confirmed" });
const messageV0 = new TransactionMessage({
payerKey: config.rewardsKeypair.publicKey,
recentBlockhash: bhInfo.value.blockhash,
instructions: [
...createComputeBudgetInstruction(100_000, 100),
...instruction.instructions
],
}).compileToV0Message();
const tx = new VersionedTransaction(messageV0);
tx.sign([config.rewardsKeypair]);
const simulation = await connection.simulateTransaction(tx, { commitment: "confirmed" });
console.log("Simulation: ", simulation.value);
if (simulation.value.err === "BlockhashNotFound") {
throw new Error("Blockhash not found. Try again.");
}
if (simulation.value.err) {
throw simulation.value.err;
}
try {
const signature = await connection.sendTransaction(tx, {
maxRetries: 20,
skipPreflight: true,
});
const confirmation = await connection.confirmTransaction({
signature,
blockhash: bhInfo.value.blockhash,
lastValidBlockHeight: bhInfo.value.lastValidBlockHeight,
}, "confirmed");
if (confirmation.value.err) {
throw new Error(`Transaction not confirmed: ${confirmation.value.err.toString()}`);
}
console.log("Confirmed: ", signature);
} catch (error) {
console.error("Failed to send rewards: ", error);
throw error;
}
}
sendRewards().then(() => {
console.log("Done");
process.exit(0);
}).catch(error => {
console.error("Error: ", error);
process.exit(1);
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment