diff --git a/package.json b/package.json index 3080edca..2c72e547 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dappio-wonderland/navigator", - "version": "0.2.14", + "version": "0.2.14-test.13", "description": "Dappio Navigator: The Universal Typescript Client for Instantiating DeFi Protocols", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -21,7 +21,8 @@ "testFriktion": "mocha --require ts-node/register --timeout 10000000 ./test/friktion.ts", "testNftFinance": "mocha --require ts-node/register --timeout 10000000 ./test/nftFinance.ts", "testLido": "mocha --require ts-node/register --timeout 10000000 ./test/lido.ts", - "testMarinade": "mocha --require ts-node/register --timeout 10000000 ./test/marinade.ts" + "testMarinade": "mocha --require ts-node/register --timeout 10000000 ./test/marinade.ts", + "testGenopets": "mocha --require ts-node/register --timeout 10000000 ./test/genopets.ts" }, "keywords": [], "author": "", diff --git a/src/genopets/ids.ts b/src/genopets/ids.ts new file mode 100644 index 00000000..3bbdc315 --- /dev/null +++ b/src/genopets/ids.ts @@ -0,0 +1,6 @@ +import { PublicKey } from "@solana/web3.js"; + +export const GENOPETS_FARM_PROGRAM_ID = new PublicKey("StaKe9nb7aUjXpjpZ45o6uJBsZxj2BWCDBtjk8LCg2v"); + +// calculate by findProgramAddressSync([Buffer.from("stake-master-seed")], GENOPETS_FARM_PROGRAM_ID) +export const FARM_MASTER_ID = new PublicKey("tEAbLeDznDdQ5jvdk1cdm2qUzoYmyc6nX5FChAyAB2U"); diff --git a/src/genopets/index.ts b/src/genopets/index.ts new file mode 100644 index 00000000..84a08faf --- /dev/null +++ b/src/genopets/index.ts @@ -0,0 +1,101 @@ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; +import { IFarmInfo, IFarmerInfo } from "../types"; + +export * from "./ids"; +export * from "./infos"; +export * from "./layouts"; +export * from "./utils"; + +export interface FarmInfo extends IFarmInfo { + master: FarmMaster; + poolToken: PublicKey; + tokenDecimals: BN; + weight: BN; + earliestUnlockDate: BN; + usersLockingWeight: BN; + poolTokenReserve: BN; + weightPerToken: BN; + governanceEligible: boolean; +} + +export interface FarmMaster { + id: PublicKey; + authority: PublicKey; + sgeneMinter: PublicKey; + mintSgene: PublicKey; + geneMint: PublicKey; + geneRewarder: PublicKey; + totalGeneRewarded: BN; + ataGeneRewarder: PublicKey; + totalGeneAllocated: BN; + totalWeight: BN; + startTime: BN; + endTime: BN; + epochTime: BN; + decayFactorPerEpoch: BN; + initialGenesPerEpoch: BN; + stakeParams: { + minStakeDuration: BN; + maxStakeDuration: BN; + }; + pausedState: BN; + totalRewardWeight: BN; + accumulatedYieldRewardsPerWeight: BN; + lastYieldDistribution: BN; + totalGeneStaked: BN; + timeFactor: BN; +} + +export interface FarmerInfo extends IFarmerInfo { + totalRewardWeight: BN; + subYieldRewards: BN; + activeDeposits: BN; + totalRewards: BN; + currentDepositIndex: BN; + instance: FarmerInstance[]; +} + +export interface FarmerInstance { + id: PublicKey; + user: PublicKey; + amount: BN; + poolToken: PublicKey; + rewardWeight: BN; + depositTimestamp: BN; + depositMultiplier: BN; + lockFrom: BN; + lockUntil: BN; + isYield: boolean; + tokenDecimals: BN; + active: boolean; + governanceEligible: boolean; +} + +export const defaultFarmerMaster = { + id: PublicKey.default, + authority: PublicKey.default, + sgeneMinter: PublicKey.default, + mintSgene: PublicKey.default, + geneMint: PublicKey.default, + geneRewarder: PublicKey.default, + totalGeneRewarded: new BN(0), + ataGeneRewarder: PublicKey.default, + totalGeneAllocated: new BN(0), + totalWeight: new BN(0), + startTime: new BN(0), + endTime: new BN(0), + epochTime: new BN(0), + decayFactorPerEpoch: new BN(0), + initialGenesPerEpoch: new BN(0), + stakeParams: { + minStakeDuration: new BN(0), + maxStakeDuration: new BN(0), + }, + pausedState: new BN(0), + totalRewardWeight: new BN(0), + accumulatedYieldRewardsPerWeight: new BN(0), + lastYieldDistribution: new BN(0), + totalGeneStaked: new BN(0), + timeFactor: new BN(0), +}; diff --git a/src/genopets/infos.ts b/src/genopets/infos.ts new file mode 100644 index 00000000..25b80ba6 --- /dev/null +++ b/src/genopets/infos.ts @@ -0,0 +1,246 @@ +import { Connection, PublicKey, AccountInfo, DataSizeFilter, GetProgramAccountsConfig } from "@solana/web3.js"; +import { IFarmInfoWrapper, IInstanceFarm } from "../types"; +import { FARM_MASTER_ID, GENOPETS_FARM_PROGRAM_ID } from "./ids"; +import { FARMER_INSTANCE_LAYOUT, FARMER_LAYOUT, FARM_MASTER_LAYOUT, FARM_LAYOUT } from "./layouts"; +import * as types from "."; +import { getMultipleAccounts } from "../utils"; +import { getFarmerInstanceKey } from "./utils"; + +let infos: IInstanceFarm; +infos = class InstanceGenopets { + static async getAllFarms(connection: Connection): Promise { + const farmMasterAccount = await connection.getAccountInfo(FARM_MASTER_ID); + const farmMaster = this._parseFarmMaster(farmMasterAccount?.data!, FARM_MASTER_ID); + + const sizeFilter: DataSizeFilter = { + dataSize: 250, + }; + const filters = [sizeFilter]; + const config: GetProgramAccountsConfig = { filters: filters }; + const allFarms = await connection.getProgramAccounts(GENOPETS_FARM_PROGRAM_ID, config); + const farms = allFarms.map((account) => { + let farm = this.parseFarm(account.account.data, account.pubkey); + farm.master = farmMaster; + return farm; + }); + + return farms; + } + + static async getAllFarmWrappers(connection: Connection): Promise { + return (await this.getAllFarms(connection)).map((farmInfo) => new FarmInfoWrapper(farmInfo)); + } + + static async getFarm(connection: Connection, farmId: PublicKey): Promise { + const [farmMasterAccount, farmAccount] = await getMultipleAccounts(connection, [FARM_MASTER_ID, farmId]); + if (!farmMasterAccount || !farmAccount) throw "Error: Failed to get farm"; + const farmMaster = this._parseFarmMaster(farmMasterAccount.account?.data!, FARM_MASTER_ID); + let farm = this.parseFarm(farmAccount.account?.data!, farmId); + farm.master = farmMaster; + + return farm; + } + + static async getFarmWrapper(connection: Connection, farmId: PublicKey): Promise { + const farm = await this.getFarm(connection, farmId); + + return new FarmInfoWrapper(farm); + } + + static parseFarm(data: Buffer, farmId: PublicKey): types.FarmInfo { + const decodedData = FARM_LAYOUT.decode(data); + + let { + poolToken, + tokenDecimals, + weight, + earliestUnlockDate, + usersLockingWeight, + poolTokenReserve, + weightPerToken, + governanceEligible, + } = decodedData; + + return { + farmId, + master: types.defaultFarmerMaster, + poolToken, + tokenDecimals, + weight, + earliestUnlockDate, + usersLockingWeight, + poolTokenReserve, + weightPerToken, + governanceEligible, + }; + } + + private static _parseFarmMaster(data: Buffer, id: PublicKey): types.FarmMaster { + const decodedData = FARM_MASTER_LAYOUT.decode(data); + + let { + authority, + sgeneMinter, + mintSgene, + geneMint, + geneRewarder, + totalGeneRewarded, + ataGeneRewarder, + totalGeneAllocated, + totalWeight, + startTime, + endTime, + epochTime, + decayFactorPerEpoch, + initialGenesPerEpoch, + stakeParams, + pausedState, + totalRewardWeight, + accumulatedYieldRewardsPerWeight, + lastYieldDistribution, + totalGeneStaked, + timeFactor, + } = decodedData; + + return { + id, + authority, + sgeneMinter, + mintSgene, + geneMint, + geneRewarder, + totalGeneRewarded, + ataGeneRewarder, + totalGeneAllocated, + totalWeight, + startTime, + endTime, + epochTime, + decayFactorPerEpoch, + initialGenesPerEpoch, + stakeParams, + pausedState, + totalRewardWeight, + accumulatedYieldRewardsPerWeight, + lastYieldDistribution, + totalGeneStaked, + timeFactor, + }; + } + + static async getAllFarmers(connection: Connection, userKey: PublicKey): Promise { + const farmerId = PublicKey.findProgramAddressSync( + [Buffer.from("staker-seed"), userKey.toBuffer()], + GENOPETS_FARM_PROGRAM_ID + )[0]; + const farmer = await this.getFarmer(connection, farmerId); + + return [farmer]; + } + + static async getFarmerId(farmInfo: types.FarmInfo, userKey: PublicKey): Promise { + const [farmerId, _] = PublicKey.findProgramAddressSync( + [Buffer.from("staker-seed"), userKey.toBuffer()], + GENOPETS_FARM_PROGRAM_ID + ); + + return farmerId; + } + + static async getFarmer(connection: Connection, farmerId: PublicKey, version?: number): Promise { + let data = (await connection.getAccountInfo(farmerId)) as AccountInfo; + let farmer = this.parseFarmer(data.data, farmerId); + const farmerInstanceKeys: PublicKey[] = []; + for (let i = 0; i < Number(farmer.currentDepositIndex); i++) { + farmerInstanceKeys.push(getFarmerInstanceKey(farmer.userKey, i)); + } + const farmerInstanceAccounts = await getMultipleAccounts(connection, farmerInstanceKeys); + farmer.instance = farmerInstanceAccounts + .map((farmerInstance) => { + return farmerInstance.account?.data + ? this._parseFarmerInstance(farmerInstance.account?.data, farmerInstance.pubkey) + : null; + }) + .filter((farmerInstance) => Boolean(farmerInstance)) as types.FarmerInstance[]; + + return farmer; + } + + static parseFarmer(data: Buffer, farmerId: PublicKey): types.FarmerInfo { + let decodedData = FARMER_LAYOUT.decode(data); + let { owner, totalRewardWeight, subYieldRewards, activeDeposits, totalRewards, currentDepositIndex } = decodedData; + + return { + farmerId, + userKey: new PublicKey(owner), + totalRewardWeight, + subYieldRewards, + activeDeposits, + totalRewards, + currentDepositIndex, + instance: [], + }; + } + + private static _parseFarmerInstance(data: Buffer, depositId: PublicKey): types.FarmerInstance { + let decodedData = FARMER_INSTANCE_LAYOUT.decode(data); + let { + user, + amount, + poolToken, + rewardWeight, + depositTimestamp, + depositMultiplier, + lockFrom, + lockUntil, + isYield, + tokenDecimals, + active, + governanceEligible, + } = decodedData; + + return { + id: depositId, + user, + amount, + poolToken, + rewardWeight, + depositTimestamp, + depositMultiplier, + lockFrom, + lockUntil, + isYield, + tokenDecimals, + active, + governanceEligible, + }; + } +}; + +export { infos }; + +export class FarmInfoWrapper implements IFarmInfoWrapper { + constructor(public farmInfo: types.FarmInfo) {} + + getStakedAmount(): number { + // TODO + return 0; + } + + getAprs(_x: number, _y: number, _z: number): number[] { + // TODO + return []; + } +} + +export class FarmerInfoWrapper { + constructor(public farmerInfo: types.FarmerInfo) {} + + getFarmerInstance() { + return getFarmerInstanceKey(this.farmerInfo.userKey, Number(this.farmerInfo.currentDepositIndex)); + } + + getLatestFarmerInstance() { + return getFarmerInstanceKey(this.farmerInfo.userKey, Number(this.farmerInfo.currentDepositIndex) + 1); + } +} diff --git a/src/genopets/layouts.ts b/src/genopets/layouts.ts new file mode 100644 index 00000000..a2c8b954 --- /dev/null +++ b/src/genopets/layouts.ts @@ -0,0 +1,70 @@ +import { publicKey, struct, u64, u128, u8, bool, u16, i64, u32, f64, str } from "@project-serum/borsh"; +// @ts-ignore +import { blob } from "buffer-layout"; + +// StakingPool +export const FARM_LAYOUT = struct([ + u64("discriminator"), + publicKey("poolToken"), + u8("tokenDecimals"), + u32("weight"), + u64("earliestUnlockDate"), + u64("usersLockingWeight"), + u64("poolTokenReserve"), + u32("weightPerToken"), + bool("governanceEligible"), +]); + +// StakeMaster +export const FARM_MASTER_LAYOUT = struct([ + u64("discriminator"), + publicKey("authority"), + publicKey("sgeneMinter"), + publicKey("mintSgene"), + publicKey("geneMint"), + publicKey("geneRewarder"), + u64("totalGeneRewarded"), + publicKey("ataGeneRewarder"), + u64("totalGeneAllocated"), + u32("totalWeight"), + u64("startTime"), + u32("endTime"), + u64("epochTime"), + f64("decayFactorPerEpoch"), + u64("initialGenesPerEpoch"), + struct([u8("minStakeDuration"), u8("maxStakeDuration")], "stakeParams"), + bool("pausedState"), + u64("totalRewardWeight"), + u64("accumulatedYieldRewardsPerWeight"), + u64("lastYieldDistribution"), + u64("totalGeneStaked"), + u64("timeFactor"), +]); + +// Staker +export const FARMER_LAYOUT = struct([ + u64("discriminator"), + publicKey("owner"), + u64("totalRewardWeight"), + u64("subYieldRewards"), + u32("activeDeposits"), + u64("totalRewards"), + u32("currentDepositIndex"), +]); + +// Deposit +export const FARMER_INSTANCE_LAYOUT = struct([ + u64("discriminator"), + publicKey("user"), + u64("amount"), + publicKey("poolToken"), + u64("rewardWeight"), + u64("depositTimestamp"), + f64("depositMultiplier"), + u64("lockFrom"), + u64("lockUntil"), + bool("isYield"), + u8("tokenDecimals"), + bool("active"), + bool("governanceEligible"), +]); diff --git a/src/genopets/utils.ts b/src/genopets/utils.ts new file mode 100644 index 00000000..fc8c961b --- /dev/null +++ b/src/genopets/utils.ts @@ -0,0 +1,17 @@ +import { PublicKey } from "@solana/web3.js"; +import { BN } from "bn.js"; +import { GENOPETS_FARM_PROGRAM_ID } from "./ids"; + +export function getFarmerInstanceKey(userKey: PublicKey, nonce: number): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("staking-deposit"), userKey.toBuffer(), new BN(nonce).toArrayLike(Buffer, `le`, 4)], + GENOPETS_FARM_PROGRAM_ID + )[0]; +} + +export function getFarmId(mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("stake-pool-seed"), mint.toBuffer()], + GENOPETS_FARM_PROGRAM_ID + )[0]; +} diff --git a/src/index.ts b/src/index.ts index c602e105..c03e1366 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,4 @@ export * as tulip from "./tulip"; export * as nftFinance from "./nftFinance"; export * as friktion from "./friktion"; export * as lido from "./lido"; +export * as genopets from "./genopets"; diff --git a/test/genopets.ts b/test/genopets.ts new file mode 100644 index 00000000..8fc9717f --- /dev/null +++ b/test/genopets.ts @@ -0,0 +1,60 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { genopets } from "../src"; + +describe("Genopets", () => { + // const connection = new Connection("https://rpc-mainnet-fork.dappio.xyz", { + // commitment: "confirmed", + // wsEndpoint: "wss://rpc-mainnet-fork.dappio.xyz/ws", + // }); + // const connection = new Connection("https://solana-api.tt-prod.net", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + // const connection = new Connection("https://ssc-dao.genesysgo.net", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + // const connection = new Connection("https:////api.mainnet-beta.solana.com", { + // commitment: "confirmed", + // confirmTransactionInitialTimeout: 180 * 1000, + // }); + const connection = new Connection("https://rpc-mainnet-fork.epochs.studio", { + commitment: "confirmed", + confirmTransactionInitialTimeout: 180 * 1000, + wsEndpoint: "wss://rpc-mainnet-fork.epochs.studio/ws", + }); + + const userKey = new PublicKey("3bnAKgVhM1MihYpJQ6hK83BirWhVFbyhkvvCQqJ8tZ25"); + + it("Fetch all farms", async () => { + const farms = await genopets.infos.getAllFarms(connection); + const farm0 = farms[0]; + const farm = (await genopets.infos.getFarm(connection, farm0.farmId)) as genopets.FarmInfo; + console.log("farm:", farm); + console.log("farm pool token:", farm.poolToken.toBase58()); + console.log("farm master authority:", farm.master.authority.toBase58()); + console.log("farm master geneMint:", farm.master.geneMint.toBase58()); + console.log("farm master mintSgene:", farm.master.mintSgene.toBase58()); + + const farmerWrapper = new genopets.FarmInfoWrapper(farm); + const userDeposit = genopets.getFarmerInstanceKey(userKey, 0); + console.log("userDeposit:", userDeposit.toBase58()); + }); + + it("Fetch all farmers", async () => { + const farmer = (await genopets.infos.getAllFarmers(connection, userKey)) as genopets.FarmerInfo[]; + console.log("farmer:", farmer); + console.log("farmer:", farmer[0].farmerId.toBase58()); + farmer[0].instance.forEach((farmer, index) => { + if (farmer?.user.equals(userKey)) + console.log( + `# ${index}:`, + farmer?.id.toBase58(), + ", is_yield:", + farmer.isYield, + ", timestamp:", + Number(farmer.lockUntil) + ); + }); + }); +});