@zebec-network/zebec-stake-sdk
Version:
An SDK for zebec network stake solana program
546 lines (545 loc) • 26.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StakeService = exports.StakeServiceBuilder = void 0;
const anchor_1 = require("@coral-xyz/anchor");
const web3_js_1 = require("@solana/web3.js");
const core_utils_1 = require("@zebec-network/core-utils");
const solana_common_1 = require("@zebec-network/solana-common");
const assert_1 = __importDefault(require("assert"));
const bignumber_js_1 = require("bignumber.js");
const artifacts_1 = require("./artifacts");
const pda_1 = require("./pda");
const providers_1 = require("./providers");
const rateLimitQueue_1 = require("./rateLimitQueue");
const utils_1 = require("./utils");
/**
* StakeServiceBuilder is a builder class for creating a StakeService instance.
* It allows you to set the network, provider, and program to use.
*/
class StakeServiceBuilder {
_program;
_provider;
_network;
/**
*
* @param network The network to use. If not set, a default network: 'mainnet-beta' will be used.
* @returns
*/
setNetwork(network) {
if (this._network) {
throw new Error("InvalidOperation: Network is set twice.");
}
this._network = network ? network : "mainnet-beta";
return this;
}
/**
* Set the provider to use. If not set, a default provider will be created.
* @param provider The provider to use. If not set, a default provider: 'ReadonlyProvider' will be created.
* @returns The StakeServiceBuilder instance.
*/
setProvider(provider) {
if (this._provider) {
throw new Error("InvalidOperation: Provider is set twice.");
}
if (!this._network) {
throw new Error("InvalidOperation: Network is not set. Please set the network before setting the provider.");
}
if (provider) {
this.validateProviderNetwork(provider, this._network);
this._provider = provider;
}
else {
this._provider = (0, providers_1.createReadonlyProvider)(new web3_js_1.Connection((0, web3_js_1.clusterApiUrl)(this._network)));
}
return this;
}
/**
*
* @param provider The provider to compare with.
*/
validateProviderNetwork(provider, network) {
const connection = provider.connection;
const rpcEndpoint = connection.rpcEndpoint;
const connNetwork = rpcEndpoint.includes("devnet")
? "devnet"
: rpcEndpoint.includes("testnet")
? "testnet"
: rpcEndpoint.includes("localhost:8899")
? "localnet"
: "mainnet-beta";
if (network !== connNetwork) {
throw new Error(`InvalidOperation: Network mismatch. network and connection network should be same. network: ${this._network}, connection: ${connNetwork}`);
}
}
/**
* Set the program to use. If not set, a default program will be created.
* @param program The program to use. If not set, a default program will be created.
* @returns The StakeServiceBuilder instance.
*/
setProgram(createProgram) {
if (this._program) {
throw new Error("InvalidOperation: Program is set twice.");
}
if (!this._network) {
throw new Error("InvalidOperation: Network is not set. Please set the network before setting the provider.");
}
if (!this._provider) {
throw new Error("InvalidOperation: Provider is not set. Please set the provider before setting the program.");
}
this._program = !createProgram
? new anchor_1.Program(artifacts_1.ZEBEC_STAKE_IDL_V1, this._provider)
: createProgram(this._provider);
return this;
}
build() {
if (!this._network) {
throw new Error("InvalidOperation: Network is not set. Please set the network before building the service.");
}
if (!this._provider) {
throw new Error("InvalidOperation: Provider is not set. Please set the provider before building the service.");
}
if (!this._program) {
throw new Error("InvalidOperation: Program is not set. Please set the program before building the service.");
}
return new StakeService(this._provider, this._program, this._network);
}
}
exports.StakeServiceBuilder = StakeServiceBuilder;
class StakeService {
provider;
program;
network;
constructor(provider, program, network) {
this.provider = provider;
this.program = program;
this.network = network;
}
async _createPayload(payerKey, instructions, signers, addressLookupTableAccounts) {
const errorMap = new Map();
this.program.idl.errors.map((error) => errorMap.set(error.code, error.msg));
let signTransaction;
const provider = this.provider;
if (provider instanceof anchor_1.AnchorProvider) {
signTransaction = async (tx) => {
return provider.wallet.signTransaction(tx);
};
}
return new solana_common_1.TransactionPayload(this.connection, errorMap, { instructions, feePayer: payerKey, signers, addressLookupTableAccounts }, signTransaction);
}
async getInitLockupInstruction(creator, lockup, stakeToken, rewardToken, rewardVault, stakeVault, data) {
return this.program.methods
.initLockup({
fee: data.fee,
durationMap: data.rewardSchemes,
feeVault: data.feeVault,
name: data.name,
minimumStake: data.minimumStake,
})
.accountsPartial({
creator,
lockup,
rewardToken,
rewardVault,
stakeToken,
stakeVault,
})
.instruction();
}
async getUpdateLockupInstruction(updater, lockup, data) {
return this.program.methods
.updateLockup({
durationMap: data.rewardSchemes,
fee: data.fee,
feeVault: data.feeVault,
minimumStake: data.minimumStake,
})
.accountsPartial({
updater,
lockup,
})
.instruction();
}
async getStakeInstruction(feePayer, lockup, stakeToken, stakeVault, staker, userNonce, stakePda, stakeVaultTokenAccount, data) {
return this.program.methods
.stakeZbcn(data)
.accountsPartial({
stakeToken,
feePayer,
staker,
lockup,
stakeVault,
userNonce,
stakePda,
stakeVaultTokenAccount,
})
.instruction();
}
async getUnstakeInstruction(feePayer, feeVault, lockup, stakePda, rewardToken, rewardVault, stakeToken, stakeVault, staker, stakerTokenAccount, nonce) {
return this.program.methods
.unstakeZbcn(nonce)
.accountsPartial({
feePayer,
feeVault,
rewardToken,
stakeToken,
staker,
lockup,
stakeVault,
stakePda,
rewardVault,
stakerTokenAccount,
})
.instruction();
}
async initLockup(params) {
const creator = params.creator
? (0, anchor_1.translateAddress)(params.creator)
: this.provider.publicKey;
if (!creator) {
throw new Error("MissingArgument: Please provide either creator address or publicKey in provider");
}
const stakeToken = (0, anchor_1.translateAddress)(params.stakeToken);
const rewardToken = (0, anchor_1.translateAddress)(params.rewardToken);
const feeVault = (0, anchor_1.translateAddress)(params.feeVault);
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeToken);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const rewardSchemes = params.rewardSchemes.map((value) => {
return {
duration: new anchor_1.BN(value.duration),
reward: new anchor_1.BN((0, core_utils_1.percentToBps)(value.rewardRate)),
};
});
const lockup = (0, pda_1.deriveLockupAddress)(params.name, this.programId);
const rewardVault = (0, pda_1.deriveRewardVaultAddress)(lockup, this.programId);
const stakeVault = (0, pda_1.deriveStakeVaultAddress)(lockup, this.programId);
const fee = new anchor_1.BN((0, core_utils_1.percentToBps)(params.fee));
const minimumStake = new anchor_1.BN((0, bignumber_js_1.BigNumber)(params.minimumStake).times(UNITS_PER_STAKE_TOKEN).toFixed(0));
const instruction = await this.getInitLockupInstruction(creator, lockup, stakeToken, rewardToken, rewardVault, stakeVault, {
fee,
feeVault: feeVault,
name: params.name,
rewardSchemes,
minimumStake,
});
return this._createPayload(creator, [instruction]);
}
async updateLockup(params) {
const updater = params.updater
? (0, anchor_1.translateAddress)(params.updater)
: this.provider.publicKey;
if (!updater) {
throw new Error("MissingArgument: Please provide either updater address or publicKey in provider");
}
const lockup = (0, pda_1.deriveLockupAddress)(params.lockupName, this.programId);
const lockupInfo = await this.program.account.lockup.fetch(lockup, this.connection.commitment);
const stakeToken = lockupInfo.stakedToken.tokenAddress;
const feeVault = (0, anchor_1.translateAddress)(params.feeVault);
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeToken);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const fee = new anchor_1.BN((0, core_utils_1.percentToBps)(params.fee));
const minimumStake = new anchor_1.BN((0, bignumber_js_1.BigNumber)(params.minimumStake).times(UNITS_PER_STAKE_TOKEN).toFixed(0));
const rewardSchemes = params.rewardSchemes.map((value) => {
return {
duration: new anchor_1.BN(value.duration),
reward: new anchor_1.BN((0, core_utils_1.percentToBps)(value.rewardRate)),
};
});
const instruction = await this.getUpdateLockupInstruction(updater, lockup, {
fee,
feeVault,
minimumStake,
rewardSchemes,
});
return this._createPayload(updater, [instruction]);
}
async stake(params) {
const staker = params.staker
? (0, anchor_1.translateAddress)(params.staker)
: this.provider.publicKey;
if (!staker) {
throw new Error("MissingArgument: Please provide either staker address or publicKey in provider");
}
const feePayer = params.feePayer
? (0, anchor_1.translateAddress)(params.feePayer)
: staker;
const lockup = (0, pda_1.deriveLockupAddress)(params.lockupName, this.programId);
const lockupAccount = await this.program.account.lockup.fetchNullable(lockup, this.connection.commitment);
if (!lockupAccount) {
throw new Error(`Lockup account does not exists for address: ${lockup}`);
}
const lockPeriods = lockupAccount.stakeInfo.durationMap.map((item) => item.duration.toNumber());
if (!lockPeriods.includes(params.lockPeriod)) {
throw new Error(`Invalid lockperiod. Available options are: ${lockPeriods.map((l) => l.toString()).join(", ")}`);
}
const stakeToken = lockupAccount.stakedToken.tokenAddress;
const stakeVault = (0, pda_1.deriveStakeVaultAddress)(lockup, this.programId);
const userNonce = (0, pda_1.deriveUserNonceAddress)(staker, lockup, this.programId);
const userNonceAccount = await this.program.account.userNonce.fetchNullable(userNonce, this.connection.commitment);
let nonce = BigInt(0);
if (userNonceAccount) {
nonce = BigInt(userNonceAccount.nonce.toString());
}
const stakePda = (0, pda_1.deriveStakeAddress)(staker, lockup, nonce, this.programId);
const stakeVaultTokenAccount = (0, solana_common_1.getAssociatedTokenAddressSync)(stakeToken, stakeVault, true);
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeToken);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const instruction = await this.getStakeInstruction(feePayer, lockup, stakeToken, stakeVault, staker, userNonce, stakePda, stakeVaultTokenAccount, {
amount: new anchor_1.BN((0, bignumber_js_1.BigNumber)(params.amount).times(UNITS_PER_STAKE_TOKEN).toFixed(0)),
lockPeriod: new anchor_1.BN(params.lockPeriod),
nonce: new anchor_1.BN(params.nonce.toString()),
});
return this._createPayload(feePayer, [instruction]);
}
async unstake(params) {
const staker = params.staker
? (0, anchor_1.translateAddress)(params.staker)
: this.provider.publicKey;
if (!staker) {
throw new Error("MissingArgument: Please provide either staker address or publicKey in provider");
}
const feePayer = params.feePayer
? (0, anchor_1.translateAddress)(params.feePayer)
: staker;
const lockup = (0, pda_1.deriveLockupAddress)(params.lockupName, this.programId);
const lockupAccount = await this.program.account.lockup.fetchNullable(lockup, this.connection.commitment);
if (!lockupAccount) {
throw new Error(`Lockup account does not exists for address: ${lockup}`);
}
const stakeToken = lockupAccount.stakedToken.tokenAddress;
const rewardToken = lockupAccount.rewardToken.tokenAddress;
const feeVault = lockupAccount.feeInfo.feeVault;
const stakePda = (0, pda_1.deriveStakeAddress)(staker, lockup, params.nonce, this.programId);
const rewardVault = (0, pda_1.deriveRewardVaultAddress)(lockup, this.programId);
const stakeVault = (0, pda_1.deriveStakeVaultAddress)(lockup, this.programId);
const stakerTokenAccount = (0, solana_common_1.getAssociatedTokenAddressSync)(stakeToken, staker, true);
const instruction = await this.getUnstakeInstruction(feePayer, feeVault, lockup, stakePda, rewardToken, rewardVault, stakeToken, stakeVault, staker, stakerTokenAccount, new anchor_1.BN(params.nonce.toString()));
return this._createPayload(feePayer, [instruction]);
}
async getLockupInfo(lockupAddress) {
const lockupAccount = await this.program.account.lockup.fetchNullable(lockupAddress, this.connection.commitment);
if (!lockupAccount) {
return null;
}
const stakeTokenAddress = lockupAccount.stakedToken.tokenAddress;
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeTokenAddress);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
return {
address: lockupAddress.toString(),
feeInfo: {
fee: (0, core_utils_1.bpsToPercent)(lockupAccount.feeInfo.fee.toString()),
feeVault: lockupAccount.feeInfo.feeVault.toString(),
},
rewardToken: {
tokenAddress: lockupAccount.rewardToken.tokenAddress.toString(),
},
stakeToken: {
tokenAdress: lockupAccount.stakedToken.tokenAddress.toString(),
totalStaked: (0, bignumber_js_1.BigNumber)(lockupAccount.stakedToken.totalStaked.toString())
.div(UNITS_PER_STAKE_TOKEN)
.toFixed(),
},
stakeInfo: {
name: lockupAccount.stakeInfo.name,
creator: lockupAccount.stakeInfo.creator.toString(),
rewardSchemes: lockupAccount.stakeInfo.durationMap.map((value) => ({
duration: value.duration.toNumber(),
rewardRate: (0, core_utils_1.bpsToPercent)(value.reward.toString()),
})),
minimumStake: (0, bignumber_js_1.BigNumber)(lockupAccount.stakeInfo.minimumStake.toString())
.div(UNITS_PER_STAKE_TOKEN)
.toFixed(),
},
};
}
async getStakeInfo(stakeAddress, lockupAddress) {
const lockupAccount = await this.program.account.lockup.fetchNullable(lockupAddress, this.connection.commitment);
if (!lockupAccount) {
throw new Error(`Lockup account does not exists for address: ${lockupAddress}`);
}
const stakeTokenAddress = lockupAccount.stakedToken.tokenAddress;
const rewardTokenAddress = lockupAccount.rewardToken.tokenAddress;
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeTokenAddress);
const rewardTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, rewardTokenAddress);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const UNITS_PER_REWARD_TOKEN = solana_common_1.TEN_BIGNUM.pow(rewardTokenDecimals);
const stakeAccount = await this.program.account.userStakeData.fetchNullable(stakeAddress, this.connection.commitment);
if (!stakeAccount) {
return null;
}
return {
address: stakeAddress.toString(),
nonce: BigInt(stakeAccount.nonce.toString()),
createdTime: stakeAccount.createdTime.toNumber(),
stakedAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.stakedAmount.toString())
.div(UNITS_PER_STAKE_TOKEN)
.toFixed(),
rewardAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.rewardAmount.toString())
.div(UNITS_PER_REWARD_TOKEN)
.toFixed(),
stakeClaimed: stakeAccount.stakeClaimed,
lockPeriod: stakeAccount.lockPeriod.toNumber(),
lockup: stakeAccount.lockup.toString(),
staker: stakeAccount.staker.toString(),
};
}
async getUserNonceInfo(userNonceAddress) {
const userNonceAccount = await this.program.account.userNonce.fetchNullable(userNonceAddress, this.connection.commitment);
if (!userNonceAccount) {
return null;
}
return {
address: userNonceAddress.toString(),
nonce: BigInt(userNonceAccount.nonce.toString()),
};
}
async getAllStakesInfoOfUser(userAdress, lockupAddress, options = {}) {
const lockupAccount = await this.program.account.lockup.fetchNullable(lockupAddress, this.connection.commitment);
if (!lockupAccount) {
throw new Error(`Lockup account does not exists for address: ${lockupAddress}`);
}
const stakeTokenAddress = lockupAccount.stakedToken.tokenAddress;
const rewardTokenAddress = lockupAccount.rewardToken.tokenAddress;
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeTokenAddress);
const rewardTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, rewardTokenAddress);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const UNITS_PER_REWARD_TOKEN = solana_common_1.TEN_BIGNUM.pow(rewardTokenDecimals);
const userNonceAddress = (0, pda_1.deriveUserNonceAddress)(userAdress, lockupAddress, this.programId);
const userNonceAccount = await this.program.account.userNonce.fetchNullable(userNonceAddress, this.connection.commitment);
if (!userNonceAccount) {
return [];
}
const currentNonce = userNonceAccount.nonce.toNumber();
const nonces = Array.from({ length: currentNonce }, (_, i) => BigInt(i));
const stakeAddresses = nonces.map((nonce) => (0, pda_1.deriveStakeAddress)(userAdress, lockupAddress, nonce, this.programId));
const stakeAddressesChunks = (0, utils_1.chunkArray)(stakeAddresses, 100);
const stakeWithHash2D = [];
for (const stakeAddresses of stakeAddressesChunks) {
const accountInfos = await this.connection.getMultipleAccountsInfo(stakeAddresses, {
commitment: this.connection.commitment,
});
const stakeAccountsInfo = accountInfos.map((value, i) => {
(0, assert_1.default)(value, "Account does not exists for stake address: " +
stakeAddresses[i] +
" at nonce: " +
nonces[i]);
const stakeAccount = this.program.coder.accounts.decode(this.program.idl.accounts[2].name, value.data);
const info = {
address: stakeAddresses[i].toString(),
nonce: BigInt(stakeAccount.nonce.toString()),
createdTime: stakeAccount.createdTime.toNumber(),
stakedAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.stakedAmount.toString())
.div(UNITS_PER_STAKE_TOKEN)
.toFixed(),
rewardAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.rewardAmount.toString())
.div(UNITS_PER_REWARD_TOKEN)
.toFixed(),
stakeClaimed: stakeAccount.stakeClaimed,
lockPeriod: stakeAccount.lockPeriod.toNumber(),
lockup: stakeAccount.lockup.toString(),
staker: stakeAccount.staker.toString(),
};
return info;
});
const stakesWithHash = new Array(stakeAccountsInfo.length);
const { maxConcurrent = 3, minDelayMs = 400 } = options;
const queue = new rateLimitQueue_1.RateLimitedQueue(maxConcurrent, minDelayMs); // Max 3 concurrent, 300ms between requests
const promises = stakeAccountsInfo.map((stakeInfo, index) => queue.add(async () => {
const signature = await this.getStakeSignatureForStake(stakeInfo);
stakesWithHash[index] = {
hash: signature ? signature : "",
...stakeInfo,
};
}));
await Promise.all(promises);
stakeWithHash2D.push(stakesWithHash);
}
return stakeWithHash2D.flat();
}
async getAllStakesCount(lockupAddress) {
const dataSize = this.program.account.userStakeData.size;
const accountInfos = await this.connection.getProgramAccounts(this.programId, {
commitment: this.connection.commitment,
dataSlice: {
length: 0,
offset: 0,
},
filters: [
{
dataSize,
},
{
memcmp: {
bytes: lockupAddress.toString(),
offset: 81,
},
},
],
});
return accountInfos.length;
}
async getStakeSignatureForStake(stakeInfo) {
const commitment = this.connection.commitment === "finalized" ? "finalized" : "confirmed";
const signatures = await (0, utils_1.callWithEnhancedBackoff)(async () => this.connection.getSignaturesForAddress((0, anchor_1.translateAddress)(stakeInfo.address), {}, commitment));
const stakeSignatures = signatures.filter((s) => {
return !s.err && (s.blockTime ?? 0) === stakeInfo.createdTime;
});
const signatureInfo = stakeSignatures[stakeSignatures.length - 1];
return signatureInfo ? signatureInfo.signature : null;
}
async getAllStakesInfo(lockupAddress) {
const lockupAccount = await this.program.account.lockup.fetchNullable(lockupAddress, this.connection.commitment);
if (!lockupAccount) {
throw new Error(`Lockup account does not exists for address: ${lockupAddress}`);
}
const stakeTokenAddress = lockupAccount.stakedToken.tokenAddress;
const rewardTokenAddress = lockupAccount.rewardToken.tokenAddress;
const stakeTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, stakeTokenAddress);
const rewardTokenDecimals = await (0, solana_common_1.getMintDecimals)(this.connection, rewardTokenAddress);
const UNITS_PER_STAKE_TOKEN = solana_common_1.TEN_BIGNUM.pow(stakeTokenDecimals);
const UNITS_PER_REWARD_TOKEN = solana_common_1.TEN_BIGNUM.pow(rewardTokenDecimals);
const dataSize = this.program.account.userStakeData.size;
const accountInfos = await this.connection.getProgramAccounts(this.programId, {
commitment: "finalized",
filters: [
{
dataSize,
},
{
memcmp: {
bytes: lockupAddress.toString(),
offset: 81,
},
},
],
});
return accountInfos.map((accountInfo) => {
const stakeAccount = this.program.coder.accounts.decode(this.program.idl.accounts[2].name, accountInfo.account.data);
const info = {
address: accountInfo.pubkey.toString(),
nonce: BigInt(stakeAccount.nonce.toString()),
createdTime: stakeAccount.createdTime.toNumber(),
stakedAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.stakedAmount.toString())
.div(UNITS_PER_STAKE_TOKEN)
.toFixed(),
rewardAmount: (0, bignumber_js_1.BigNumber)(stakeAccount.rewardAmount.toString())
.div(UNITS_PER_REWARD_TOKEN)
.toFixed(),
stakeClaimed: stakeAccount.stakeClaimed,
lockPeriod: stakeAccount.lockPeriod.toNumber(),
lockup: stakeAccount.lockup.toString(),
staker: stakeAccount.staker.toString(),
};
return info;
});
}
get programId() {
return this.program.programId;
}
get connection() {
return this.provider.connection;
}
}
exports.StakeService = StakeService;