-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move derivation utils to the separate module
- Loading branch information
1 parent
90fd7e6
commit ea7d43b
Showing
5 changed files
with
291 additions
and
74 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
export { default as SolanaStakingClient } from "./solana/client.js"; | ||
export * from "./solana/utils.js"; | ||
export * from "./solana/types.js"; | ||
export * from "./solana/lib/derive-accounts.js"; | ||
export * from "./solana/lib/rewards.js"; | ||
export * as constants from "./solana/constants.js"; |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { PublicKey } from "@solana/web3.js"; | ||
// eslint-disable-next-line no-restricted-imports | ||
import BN from "bn.js"; | ||
|
||
import { | ||
STAKE_POOL_PREFIX, | ||
STAKE_VAULT_PREFIX, | ||
STAKE_MINT_PREFIX, | ||
STAKE_ENTRY_PREFIX, | ||
REWARD_POOL_PREFIX, | ||
REWARD_VAULT_PREFIX, | ||
REWARD_ENTRY_PREFIX, | ||
CONFIG_PREFIX, | ||
FEE_VALUE_PREFIX, | ||
} from "../constants.js"; | ||
|
||
export const deriveStakePoolPDA = ( | ||
programId: PublicKey, | ||
mint: PublicKey, | ||
authority: PublicKey, | ||
nonce: number, | ||
): PublicKey => { | ||
return PublicKey.findProgramAddressSync( | ||
[STAKE_POOL_PREFIX, mint.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)], | ||
programId, | ||
)[0]; | ||
}; | ||
|
||
export const deriveStakeVaultPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync([STAKE_VAULT_PREFIX, stakePool.toBuffer()], programId)[0]; | ||
}; | ||
|
||
export const deriveStakeMintPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync([STAKE_MINT_PREFIX, stakePool.toBuffer()], programId)[0]; | ||
}; | ||
|
||
export const deriveStakeEntryPDA = ( | ||
programId: PublicKey, | ||
stakePool: PublicKey, | ||
authority: PublicKey, | ||
nonce: number, | ||
): PublicKey => { | ||
return PublicKey.findProgramAddressSync( | ||
[STAKE_ENTRY_PREFIX, stakePool.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 4)], | ||
programId, | ||
)[0]; | ||
}; | ||
|
||
export const deriveRewardPoolPDA = ( | ||
programId: PublicKey, | ||
stakePool: PublicKey, | ||
mint: PublicKey, | ||
nonce: number, | ||
): PublicKey => { | ||
return PublicKey.findProgramAddressSync( | ||
[REWARD_POOL_PREFIX, stakePool.toBuffer(), mint.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)], | ||
programId, | ||
)[0]; | ||
}; | ||
|
||
export const deriveRewardVaultPDA = (programId: PublicKey, rewardPool: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync([REWARD_VAULT_PREFIX, rewardPool.toBuffer()], programId)[0]; | ||
}; | ||
|
||
export const deriveRewardEntryPDA = (programId: PublicKey, rewardPool: PublicKey, stakeEntry: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync( | ||
[REWARD_ENTRY_PREFIX, rewardPool.toBuffer(), stakeEntry.toBuffer()], | ||
programId, | ||
)[0]; | ||
}; | ||
|
||
export const deriveConfigPDA = (programId: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync([CONFIG_PREFIX], programId)[0]; | ||
}; | ||
|
||
export const deriveFeeValuePDA = (programId: PublicKey, target: PublicKey): PublicKey => { | ||
return PublicKey.findProgramAddressSync([FEE_VALUE_PREFIX, target.toBuffer()], programId)[0]; | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
import { ProgramAccount } from "@coral-xyz/anchor"; | ||
import { PublicKey } from "@solana/web3.js"; | ||
// eslint-disable-next-line no-restricted-imports | ||
import BN from "bn.js"; | ||
|
||
import { RewardEntry, StakeEntry, RewardPool } from "../types.js"; | ||
import { SCALE_PRECISION_FACTOR_BN } from "../constants.js"; | ||
|
||
export const REWARD_AMOUNT_PRECISION_FACTOR = new BN("1000000000"); | ||
|
||
export class RewardEntryAccumulator implements RewardEntry { | ||
lastAccountedTs: BN; | ||
|
||
claimedAmount: BN; | ||
|
||
accountedAmount: BN; | ||
|
||
rewardPool: PublicKey; | ||
|
||
stakeEntry: PublicKey; | ||
|
||
createdTs: BN; | ||
|
||
lastRewardAmount: BN; | ||
|
||
lastRewardPeriod: BN; | ||
|
||
buffer: number[]; | ||
|
||
constructor(public delegate: RewardEntry) { | ||
this.lastAccountedTs = delegate.lastAccountedTs; | ||
this.claimedAmount = delegate.claimedAmount; | ||
this.accountedAmount = delegate.accountedAmount; | ||
this.rewardPool = delegate.rewardPool; | ||
this.stakeEntry = delegate.stakeEntry; | ||
this.createdTs = delegate.createdTs; | ||
this.buffer = delegate.buffer; | ||
this.lastRewardAmount = delegate.lastRewardAmount; | ||
this.lastRewardPeriod = delegate.lastRewardPeriod; | ||
} | ||
|
||
// Calculate accountable amount by calculating how many seconds have passed since last claim/stake time | ||
getAccountableAmount( | ||
stakedTs: BN, | ||
accountableTs: BN, | ||
effectiveStakedAmount: BN, | ||
rewardAmount: BN, | ||
rewardPeriod: BN, | ||
): BN { | ||
const lastAccountedTs = this.lastAccountedTs.gt(new BN(0)) ? this.lastAccountedTs : stakedTs; | ||
const secondsPassed = accountableTs.sub(lastAccountedTs); | ||
|
||
if (secondsPassed.lt(rewardPeriod)) { | ||
return new BN(0); | ||
} | ||
|
||
const periodsPassed = secondsPassed.div(rewardPeriod); | ||
|
||
const claimablePerEffectiveStake = periodsPassed.mul(rewardAmount); | ||
|
||
const accountableAmount = claimablePerEffectiveStake.mul(effectiveStakedAmount).div(SCALE_PRECISION_FACTOR_BN); | ||
|
||
return accountableAmount; | ||
} | ||
|
||
// Calculates claimable amount from accountable amount. | ||
getClaimableAmount(): BN { | ||
const claimedAmount = this.claimedAmount.mul(REWARD_AMOUNT_PRECISION_FACTOR); | ||
const nonClaimedAmount = this.accountedAmount.sub(claimedAmount); | ||
const claimableAmount = nonClaimedAmount.div(REWARD_AMOUNT_PRECISION_FACTOR); | ||
|
||
return claimableAmount; | ||
} | ||
|
||
// Get the time of the last unlock | ||
getLastAccountedTs(stakedTs: BN, claimableTs: BN, rewardPeriod: BN): BN { | ||
const lastAccountedTs = this.lastAccountedTs.gtn(0) ? this.lastAccountedTs : stakedTs; | ||
const totalSecondsPassed = claimableTs.sub(lastAccountedTs); | ||
const periodsPassed = totalSecondsPassed.div(rewardPeriod); | ||
const periodsToSeconds = periodsPassed.mul(rewardPeriod); | ||
const currClaimTs = lastAccountedTs.add(periodsToSeconds); | ||
|
||
return currClaimTs; | ||
} | ||
|
||
// Adds accounted amount | ||
addAccountedAmount(accountedAmount: BN): void { | ||
this.accountedAmount = this.accountedAmount.add(accountedAmount); | ||
} | ||
|
||
// Adds claimed amount | ||
addClaimedAmount(claimedAmount: BN): void { | ||
this.claimedAmount = this.claimedAmount.add(claimedAmount); | ||
} | ||
} | ||
|
||
const createDefaultRewardEntry = ( | ||
stakeEntry: ProgramAccount<StakeEntry>, | ||
rewardPool: ProgramAccount<RewardPool>, | ||
): RewardEntry => { | ||
return { | ||
stakeEntry: new PublicKey(stakeEntry.publicKey), | ||
rewardPool: new PublicKey(rewardPool.publicKey), | ||
createdTs: stakeEntry.account.createdTs, | ||
lastAccountedTs: new BN(0), | ||
lastRewardAmount: new BN(0), | ||
lastRewardPeriod: new BN(0), | ||
accountedAmount: new BN(0), | ||
claimedAmount: new BN(0), | ||
buffer: [], | ||
}; | ||
}; | ||
|
||
export const calcRewards = ( | ||
rewardEntryAccount: ProgramAccount<RewardEntry> | undefined, | ||
stakeEntryAccount: ProgramAccount<StakeEntry>, | ||
rewardPoolAccount: ProgramAccount<RewardPool>, | ||
) => { | ||
const rewardEntry: RewardEntry = | ||
rewardEntryAccount?.account ?? createDefaultRewardEntry(stakeEntryAccount, rewardPoolAccount); | ||
const stakeEntry = stakeEntryAccount.account; | ||
const rewardPool = rewardPoolAccount.account; | ||
|
||
const rewardEntryAccumulator = new RewardEntryAccumulator(rewardEntry); | ||
if (rewardEntryAccumulator.createdTs.lt(stakeEntry.createdTs)) { | ||
throw new Error("InvalidRewardEntry"); | ||
} | ||
|
||
const currTs = Math.floor(Date.now() / 1000); | ||
|
||
const stakedTs = rewardPool.createdTs ? BN.max(stakeEntry.createdTs, rewardPool.createdTs) : stakeEntry.createdTs; | ||
const claimableTs = stakeEntry.closedTs.gtn(0) ? stakeEntry.closedTs : new BN(currTs); | ||
|
||
const amountUpdated = | ||
!rewardPool.rewardAmount.eq(rewardPool.lastRewardAmount) && | ||
rewardPool.lastAmountUpdateTs.gt(stakeEntry.createdTs) && | ||
rewardPool.lastAmountUpdateTs.gt(stakeEntry.closedTs); | ||
const periodUpdated = | ||
!rewardPool.rewardPeriod.eq(rewardPool.lastRewardPeriod) && | ||
rewardPool.lastPeriodUpdateTs.gt(stakeEntry.createdTs) && | ||
rewardPool.lastPeriodUpdateTs.gt(stakeEntry.closedTs); | ||
|
||
if (amountUpdated || periodUpdated) { | ||
let firstUpdateTs: BN, secondUpdateTs: BN, rewardAmount: BN, rewardPeriod: BN; | ||
if (amountUpdated && periodUpdated) { | ||
if (rewardPool.lastAmountUpdateTs.lt(rewardPool.lastPeriodUpdateTs)) { | ||
firstUpdateTs = rewardPool.lastAmountUpdateTs; | ||
secondUpdateTs = rewardPool.lastPeriodUpdateTs; | ||
rewardAmount = rewardPool.rewardAmount; | ||
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod; | ||
} else { | ||
firstUpdateTs = rewardPool.lastPeriodUpdateTs; | ||
secondUpdateTs = rewardPool.lastAmountUpdateTs; | ||
rewardAmount = rewardEntryAccumulator.lastRewardAmount; | ||
rewardPeriod = rewardPool.rewardPeriod; | ||
} | ||
} else if (amountUpdated) { | ||
firstUpdateTs = new BN(0); | ||
secondUpdateTs = rewardPool.lastAmountUpdateTs; | ||
rewardAmount = rewardEntryAccumulator.lastRewardAmount; | ||
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod; | ||
} else { | ||
firstUpdateTs = new BN(0); | ||
secondUpdateTs = rewardPool.lastPeriodUpdateTs; | ||
rewardAmount = rewardEntryAccumulator.lastRewardAmount; | ||
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod; | ||
} | ||
|
||
if (firstUpdateTs.gtn(0)) { | ||
const firstAccountableAmount = rewardEntryAccumulator.getAccountableAmount( | ||
stakedTs, | ||
firstUpdateTs, | ||
stakeEntry.effectiveAmount, | ||
rewardEntryAccumulator.lastRewardAmount, | ||
rewardEntryAccumulator.lastRewardPeriod, | ||
); | ||
rewardEntryAccumulator.addAccountedAmount(firstAccountableAmount); | ||
rewardEntryAccumulator.lastAccountedTs = rewardEntryAccumulator.getLastAccountedTs( | ||
stakedTs, | ||
firstUpdateTs, | ||
rewardPool.lastRewardPeriod, | ||
); | ||
} | ||
const secondAccountableAmount = rewardEntryAccumulator.getAccountableAmount( | ||
stakedTs, | ||
secondUpdateTs, | ||
stakeEntry.effectiveAmount, | ||
rewardAmount, | ||
rewardPeriod, | ||
); | ||
rewardEntryAccumulator.addAccountedAmount(secondAccountableAmount); | ||
rewardEntryAccumulator.lastAccountedTs = rewardEntryAccumulator.getLastAccountedTs( | ||
stakedTs, | ||
secondUpdateTs, | ||
rewardPeriod, | ||
); | ||
} | ||
|
||
const accountableAmount = rewardEntryAccumulator.getAccountableAmount( | ||
stakedTs, | ||
claimableTs, | ||
stakeEntry.effectiveAmount, | ||
rewardPool.rewardAmount, | ||
rewardPool.rewardPeriod, | ||
); | ||
rewardEntryAccumulator.addAccountedAmount(accountableAmount); | ||
|
||
return rewardEntryAccumulator.getClaimableAmount(); | ||
}; |
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