Last active
May 15, 2024 19:42
-
-
Save NotoriousPyro/1675b1de59ce2496411e4a654bcc2b79 to your computer and use it in GitHub Desktop.
Solana Rewards distribution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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