UNPKG

@zebec-network/zebec-stake-sdk

Version:

An SDK for zebec network stake solana program

473 lines (472 loc) 24.3 kB
"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 assert_1 = __importDefault(require("assert")); const bignumber_js_1 = require("bignumber.js"); 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 artifacts_1 = require("./artifacts"); const constants_1 = require("./constants"); 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.forEach((error) => errorMap.set(error.code, error.msg)); let signTransaction = undefined; 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, 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 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 = constants_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, bignumber_js_1.BigNumber)(params.fee).times(UNITS_PER_STAKE_TOKEN).toFixed(0)); 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 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()).concat(", ")); } 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 = constants_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(staker, [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(staker, [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 = constants_1.TEN_BIGNUM.pow(stakeTokenDecimals); return { address: lockupAddress.toString(), feeInfo: { fee: (0, bignumber_js_1.BigNumber)(lockupAccount.feeInfo.fee.toString()).div(UNITS_PER_STAKE_TOKEN).toFixed(), 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 = constants_1.TEN_BIGNUM.pow(stakeTokenDecimals); const UNITS_PER_REWARD_TOKEN = constants_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 = constants_1.TEN_BIGNUM.pow(stakeTokenDecimals); const UNITS_PER_REWARD_TOKEN = constants_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); let 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; }); let 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 = constants_1.TEN_BIGNUM.pow(stakeTokenDecimals); const UNITS_PER_REWARD_TOKEN = constants_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;