Skip to content

Commit

Permalink
Merge pull request #24 from everstake/feat/solana_improve
Browse files Browse the repository at this point in the history
improved unstake and claim methods
  • Loading branch information
Messer4 authored Nov 19, 2024
2 parents 8f0dbdb + 08c0c7b commit e0fc51b
Show file tree
Hide file tree
Showing 3 changed files with 393 additions and 13 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"bs58": "^5.0.0",
"web3": "^4.14.0",
"btc-staking-ts": "^0.2.10",
"superstruct": "^0.16.0",
"ethereum-multicall": "^2.26.0"
}
}
243 changes: 230 additions & 13 deletions solana.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ const {
Keypair,
PublicKey,
StakeProgram,
Lockup,
Transaction,
TransactionMessage,
VersionedTransaction,
LAMPORTS_PER_SOL,
TransactionInstruction
} = require('@solana/web3.js');

const BigNumber = require('bignumber.js');
const {parsedAccountInfoToStakeAccount, isLockupInForce, stakeAccountState, StakeState} = require("./solana_stake_account.js");
const {CheckToken, ERROR_TEXT, SetStats} = require("./utils/api");

const NETWORKS = {
Expand Down Expand Up @@ -43,18 +46,19 @@ async function connect() {
* @param {string} address - account blockchain address (staker)
* @param {number} lamports - lamport amount
* @param {string | null} source - stake source
* @param {object | null} lockupParams - stake account lockup params
* @returns {Promise<object>} Promise object Tx
*/
async function createAccount(address, lamports, source = '0') {
async function createAccount(address, lamports, source = '0', lockupParams = Lockup.default) {
try {
await connect();
const senderPublicKey = new PublicKey(address);

const minimumRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);

lockupParams = lockupParams === null ? Lockup.default : lockupParams;
const [createStakeAccountTx, stakeAccountPublicKey, externalSigners] = source === null
? await createAccountTx(senderPublicKey, lamports + minimumRent)
: await createWithSeedTx(senderPublicKey, lamports + minimumRent, source);
? await createAccountTx(senderPublicKey, lamports + minimumRent, lockupParams)
: await createWithSeedTx(senderPublicKey, lamports + minimumRent, source, lockupParams);

const versionedTX = await prepareTransaction(createStakeAccountTx.instructions, senderPublicKey, externalSigners);
return { result: { versionedTX, stakeAccount: stakeAccountPublicKey.toString() } };
Expand All @@ -63,23 +67,24 @@ async function createAccount(address, lamports, source = '0') {
}
}

async function createAccountTx(address, lamports) {
async function createAccountTx(address, lamports, {unixTimestamp, epoch}) {
const blockhash = await getBlockhash();
const stakeAccount = Keypair.generate();
let createStakeAccountTx = StakeProgram.createAccount({
authorized: new Authorized(address, address),
fromPubkey: address,
lamports: lamports,
stakePubkey: stakeAccount.publicKey,
// SDK don't support custodian lockups
lockup: new Lockup(unixTimestamp, epoch, PublicKey.default)
});
createStakeAccountTx.recentBlockhash = blockhash;
createStakeAccountTx.sign(stakeAccount);

return [createStakeAccountTx, stakeAccount.publicKey, [stakeAccount]]
}

async function createWithSeedTx(authorityPublicKey, lamports, source) {

async function createWithSeedTx(authorityPublicKey, lamports, source, {unixTimestamp, epoch}) {
// Format source to
seed = formatSource(source);

Expand All @@ -96,13 +101,63 @@ async function createWithSeedTx(authorityPublicKey, lamports, source) {
basePubkey: authorityPublicKey,
stakePubkey: stakeAccountPubkey,

// SDK don't support custodian lockups
lockup: new Lockup(unixTimestamp, epoch, PublicKey.default),

seed: seed,
lamports: lamports,
}));

return [createStakeAccountTx, stakeAccountPubkey, []]
}

async function split(authorityPublicKey, lamports, oldStakeAccountPubkey, source) {
// TODO add support of default split
// StakeProgram.split({
// stakePubkey: stakeAccountPublicKey,
// authorizedPubkey: senderPublicKey,
// votePubkey: validatorPubkey,
// });

// Format source to
seed = formatSource(source);

const newStakeAccountPubkey = await PublicKey.createWithSeed(
authorityPublicKey,
seed,
StakeProgram.programId,
);

const splitStakeAccountTx = new Transaction().add(
StakeProgram.splitWithSeed({
stakePubkey: oldStakeAccountPubkey,
authorizedPubkey: authorityPublicKey,
splitStakePubkey: newStakeAccountPubkey,
basePubkey: authorityPublicKey,
seed: seed,
lamports: lamports
})
);

return [splitStakeAccountTx, newStakeAccountPubkey, []]
}

async function merge(authorityPublicKey, stakeAccount1, stakeAccount2) {
const mergeStakeAccountTx = StakeProgram.merge({
stakePubkey: stakeAccount1,
sourceStakePubKey: stakeAccount2,
authorizedPubkey: authorityPublicKey
})

return [mergeStakeAccountTx]
}

async function getBlockhash() {
return await connection
.getLatestBlockhash({commitment: 'max'})
.then((res) => res.blockhash);
}

/** createAccount - create account
* @param {string} token - auth API token
* @param {string} address - account blockchain address (staker)
Expand Down Expand Up @@ -145,7 +200,7 @@ async function delegate(token, address, lamports, stakeAccount) {
}
}

/** deactivate - deactivate stake
/** deactivate - deactivate stake account
* @param {string} address - account blockchain address (staker)
* @param {string} stakeAccountPublicKey - public key
* @returns {Promise<object>} Promise object deactivation Tx
Expand Down Expand Up @@ -231,7 +286,7 @@ async function getDelegations(address) {

let accounts = [];

accounts = await connection.getParsedProgramAccounts(new PublicKey("Stake11111111111111111111111111111111111111"), {
accounts = await connection.getParsedProgramAccounts(StakeProgram.programId, {
filters: [
{dataSize: 200},
{memcmp: {offset: 44, bytes: address}},
Expand All @@ -244,14 +299,18 @@ async function getDelegations(address) {
}
}

// TODO add summarised balances
async function stakeBalances(address) {}

/** stake - list of delegations
* @param {string} token - auth API token
* @param {string} sender - account blockchain address (staker)
* @param {number} lamports - lamport amount
* @param {string | null} source - stake source
* @param {object | null} lockupParams - stake account lockup params
* @returns {Promise<object>} Promise object with Versioned Tx
*/
async function stake(token, sender, lamports, source) {
async function stake(token, sender, lamports, source, lockupParams = Lockup.default) {
if (!await CheckToken(token)) {
throw new Error(ERROR_TEXT);
}
Expand All @@ -262,10 +321,10 @@ async function stake(token, sender, lamports, source) {

// Calculate how much we want to stake
const minimumRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);

lockupParams = lockupParams === null ? Lockup.default : lockupParams;
const [createStakeAccountTx, stakeAccountPublicKey, externalSigners] = source === null
? await createAccountTx(senderPublicKey, lamports + minimumRent)
: await createWithSeedTx(senderPublicKey, lamports + minimumRent, source);
? await createAccountTx(senderPublicKey, lamports + minimumRent, lockupParams)
: await createWithSeedTx(senderPublicKey, lamports + minimumRent, source, lockupParams);

const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitPrice({microLamports: 50}),
Expand All @@ -277,6 +336,11 @@ async function stake(token, sender, lamports, source) {
})
);

// cast instructions to correct JSON Serialization
tx.instructions = tx.instructions.map((instruction) => {
return new TransactionInstruction(instruction)
})

const versionedTX = await prepareTransaction(tx.instructions, senderPublicKey, externalSigners);

await SetStats(token, 'stake', lamports / LAMPORTS_PER_SOL, sender, versionedTX, chain);
Expand All @@ -287,6 +351,157 @@ async function stake(token, sender, lamports, source) {
}
}


/** unstake - unstake
* @param {string} token - auth API token
* @param {string} sender - account blockchain address (staker)
* @param {number} lamports - lamport amount
* @param {string} source - stake source
* @returns {Promise<object>} Promise object with Versioned Tx
*/
async function unstake(token, sender, lamports, source) {
if (!await CheckToken(token)) {
throw new Error(ERROR_TEXT);
}

try {
const delegations = await getDelegations(sender);

let stakeAccounts = delegations.result.map((delegationAcc) => {
return {pubkey: delegationAcc.pubkey, account: parsedAccountInfoToStakeAccount(delegationAcc.account)};
});

const epochInfo = await connection.getEpochInfo();
// Timestamp in seconds
const tm = Date.now() / 1000 | 0;

totalActiveStake = new BigNumber(0);
let activeStakeAccounts = stakeAccounts.filter((acc) => {
let isActive = !(isLockupInForce(acc.account.data.info.meta, epochInfo.epoch, tm) ||
stakeAccountState(acc.account.data, epochInfo.epoch) !== StakeState.Active);
if (isActive) totalActiveStake = totalActiveStake.plus(acc.account.data.info.stake.delegation.stake);
return isActive
});

let lamportsBN = new BigNumber(lamports)
if (totalActiveStake.lt(lamportsBN)) throw new Error('Active stake less than requested');

// Desc sorting
activeStakeAccounts.sort(function(a, b){
if (a.account.data.info.stake.delegation.stake.lte(b.account.data.info.stake.delegation.stake)) {
return a;
}

return b;
});

let accountsToDeactivate = [];
let accountsToSplit = [];
let i = 0;
while (lamportsBN.gt(new BigNumber(0)) && i < activeStakeAccounts.length) {
const lBN = new BigNumber(lamports)
const acc = activeStakeAccounts[i];
let stakeAmount = new BigNumber(acc.account.data.info.stake.delegation.stake);

// If reminder amount less than min stake amount stake account automatically become disabled
if (stakeAmount.comparedTo(lBN) <= 0 || stakeAmount.minus(lBN).lt(new BigNumber (minAmount))) {
accountsToDeactivate.push(acc);
lamportsBN = lamportsBN.minus(stakeAmount);
i++;
continue;
}

accountsToSplit.push({account: acc, lamports: lamportsBN.toNumber()});
break;
}

const senderPublicKey = new PublicKey(sender);

var instructions = [ComputeBudgetProgram.setComputeUnitPrice({microLamports: 50})];
for (var j in accountsToSplit) {
const [tx, newStakeAccountPubkey] = await split(senderPublicKey, accountsToSplit[j].lamports, accountsToSplit[j].account.pubkey, source);

const deactivateTx = StakeProgram.deactivate({
stakePubkey: newStakeAccountPubkey,
authorizedPubkey: senderPublicKey,
});

instructions.push(...tx.instructions, ...deactivateTx.instructions);
}

for (var j in accountsToDeactivate) {
const deactivateTx = StakeProgram.deactivate({
stakePubkey: accountsToDeactivate[j].pubkey,
authorizedPubkey: senderPublicKey,
});

instructions.push(...deactivateTx.instructions);
}

// cast instructions to correct JSON Serialization
instructions = instructions.map((instruction) => {
return new TransactionInstruction(instruction)
})

const versionedTX = await prepareTransaction(instructions, senderPublicKey, []);

await SetStats(token, 'unstake', lamports / LAMPORTS_PER_SOL, sender, versionedTX, chain);

return {result: versionedTX};
} catch (error) {
throw new Error(error);
}
}

async function claim(sender) {
try {
const delegations = await getDelegations(sender);

let stakeAccounts = delegations.result.map((delegationAcc) => {
return {pubkey: delegationAcc.pubkey, account: parsedAccountInfoToStakeAccount(delegationAcc.account)};
});

const epochInfo = await connection.getEpochInfo();
// Timestamp in seconds
const tm = Date.now() / 1000 | 0;

totalClaimableStake = new BigNumber(0);
let deactivatedStakeAccounts = stakeAccounts.filter((acc) => {
let isDeactivated = (!isLockupInForce(acc.account.data.info.meta, epochInfo.epoch, tm) &&
stakeAccountState(acc.account.data, epochInfo.epoch) === StakeState.Deactivated);

if (isDeactivated) totalClaimableStake = totalClaimableStake.plus(acc.account.data.info.stake.delegation.stake);
return isDeactivated
});

if (deactivatedStakeAccounts.length === 0) throw new Error('Nothing to claim');

const senderPublicKey = new PublicKey(sender);
var instructions = [ComputeBudgetProgram.setComputeUnitPrice({microLamports: 50})];
for (var j in deactivatedStakeAccounts) {

const withdrawTx = StakeProgram.withdraw({
stakePubkey: deactivatedStakeAccounts[j].pubkey,
authorizedPubkey: senderPublicKey,
toPubkey: senderPublicKey,
lamports: deactivatedStakeAccounts[j].account.lamports,
});
instructions.push(...withdrawTx.instructions);
}

// cast instructions to correct JSON Serialization
instructions = instructions.map((instruction) => {
return new TransactionInstruction(instruction)
})

const versionedTX = await prepareTransaction(instructions, senderPublicKey, []);

return {result: versionedTX};
} catch (error) {
throw new Error(error);
}
}

async function prepareTransaction(instructions, payer, externalSigners) {
const blockhash = await getBlockhash();
const messageV0 = new TransactionMessage({
Expand Down Expand Up @@ -339,6 +554,8 @@ module.exports = {
withdraw,
getDelegations,
stake,
unstake,
claim,
selectNetwork,

NETWORKS
Expand Down
Loading

0 comments on commit e0fc51b

Please sign in to comment.