From 6ad8b611ae35b64da4c6948d57584bda4da70341 Mon Sep 17 00:00:00 2001 From: phola Date: Wed, 12 Oct 2022 16:56:25 +0100 Subject: [PATCH] feat: adds getReferralPartners and code arg --- .../marinade-referral-partner-state.ts | 31 +- src/marinade.ts | 319 +++++++++++++----- test/marinade-referral-state.spec.ts | 72 +++- 3 files changed, 312 insertions(+), 110 deletions(-) diff --git a/src/marinade-referral-state/marinade-referral-partner-state.ts b/src/marinade-referral-state/marinade-referral-partner-state.ts index 0b74912..84c497d 100644 --- a/src/marinade-referral-state/marinade-referral-partner-state.ts +++ b/src/marinade-referral-state/marinade-referral-partner-state.ts @@ -1,22 +1,31 @@ -import { web3 } from '@project-serum/anchor' -import { Marinade } from '../marinade' -import { MarinadeReferralStateResponse } from './marinade-referral-state.types' +import { web3 } from "@project-serum/anchor" +import { Marinade } from "../marinade" +import { MarinadeReferralStateResponse } from "./marinade-referral-state.types" export class MarinadeReferralPartnerState { private constructor( public readonly state: MarinadeReferralStateResponse.ReferralState, public readonly referralStateAddress: web3.PublicKey, - public readonly marinadeReferralProgramId: web3.PublicKey, - ) { } + public readonly marinadeReferralProgramId: web3.PublicKey + ) {} - static async fetch(marinade: Marinade) { + static async fetch(marinade: Marinade, referralCode?: web3.PublicKey) { const { marinadeReferralProgram, config } = marinade - - if (!config.referralCode) { - throw new Error('The Referral Code must be provided in the MarinadeConfig!') + const code = referralCode ?? config.referralCode + if (!code) { + throw new Error( + "The Referral Code must be provided in the MarinadeConfig or supplied as an arg!" + ) } - const state = await marinadeReferralProgram.program.account.referralState.fetch(config.referralCode) as MarinadeReferralStateResponse.ReferralState + const state = + (await marinadeReferralProgram.program.account.referralState.fetch( + code + )) as MarinadeReferralStateResponse.ReferralState - return new MarinadeReferralPartnerState(state, config.referralCode, config.marinadeReferralProgramId) + return new MarinadeReferralPartnerState( + state, + code, + config.marinadeReferralProgramId + ) } } diff --git a/src/marinade.ts b/src/marinade.ts index 406ca77..dc05a11 100644 --- a/src/marinade.ts +++ b/src/marinade.ts @@ -1,23 +1,27 @@ -import { MarinadeConfig } from './config/marinade-config' -import { BN, Provider, Wallet, web3 } from '@project-serum/anchor' -import { MarinadeState } from './marinade-state/marinade-state' -import { getAssociatedTokenAccountAddress, getOrCreateAssociatedTokenAccount, getParsedStakeAccountInfo } from './util/anchor' -import { DepositOptions, ErrorMessage, MarinadeResult } from './marinade.types' -import { MarinadeFinanceProgram } from './programs/marinade-finance-program' -import { MarinadeReferralProgram } from './programs/marinade-referral-program' -import { MarinadeReferralPartnerState } from './marinade-referral-state/marinade-referral-partner-state' -import { MarinadeReferralGlobalState } from './marinade-referral-state/marinade-referral-global-state' -import { assertNotNullAndReturn } from './util/assert' -import { TicketAccount } from './marinade-state/borsh/ticket-account' -import { computeMsolAmount, proportionalBN } from './util' +import { MarinadeConfig } from "./config/marinade-config" +import { BN, Provider, Wallet, web3 } from "@project-serum/anchor" +import { MarinadeState } from "./marinade-state/marinade-state" +import { + getAssociatedTokenAccountAddress, + getOrCreateAssociatedTokenAccount, + getParsedStakeAccountInfo, +} from "./util/anchor" +import { DepositOptions, ErrorMessage, MarinadeResult } from "./marinade.types" +import { MarinadeFinanceProgram } from "./programs/marinade-finance-program" +import { MarinadeReferralProgram } from "./programs/marinade-referral-program" +import { MarinadeReferralPartnerState } from "./marinade-referral-state/marinade-referral-partner-state" +import { MarinadeReferralGlobalState } from "./marinade-referral-state/marinade-referral-global-state" +import { assertNotNullAndReturn } from "./util/assert" +import { TicketAccount } from "./marinade-state/borsh/ticket-account" +import { computeMsolAmount, proportionalBN } from "./util" export class Marinade { - constructor(public readonly config: MarinadeConfig = new MarinadeConfig()) { } + constructor(public readonly config: MarinadeConfig = new MarinadeConfig()) {} readonly provider: Provider = new Provider( this.config.connection, new Wallet(web3.Keypair.generate()), - { commitment: 'confirmed' }, + { commitment: "confirmed" } ) /** @@ -25,7 +29,7 @@ export class Marinade { */ readonly marinadeFinanceProgram = new MarinadeFinanceProgram( this.config.marinadeFinanceProgramId, - this.provider, + this.provider ) /** @@ -35,15 +39,19 @@ export class Marinade { this.config.marinadeReferralProgramId, this.provider, this.config.referralCode, - this, + this ) private isReferralProgram(): boolean { return this.config.referralCode != null } - private provideReferralOrMainProgram(): MarinadeFinanceProgram | MarinadeReferralProgram { - return this.isReferralProgram() ? this.marinadeReferralProgram : this.marinadeFinanceProgram + private provideReferralOrMainProgram(): + | MarinadeFinanceProgram + | MarinadeReferralProgram { + return this.isReferralProgram() + ? this.marinadeReferralProgram + : this.marinadeFinanceProgram } /** @@ -56,8 +64,10 @@ export class Marinade { /** * Fetch the Marinade referral partner's state */ - async getReferralPartnerState(): Promise { - return MarinadeReferralPartnerState.fetch(this) + async getReferralPartnerState( + referralCode?: web3.PublicKey + ): Promise { + return MarinadeReferralPartnerState.fetch(this, referralCode) } /** @@ -67,6 +77,29 @@ export class Marinade { return MarinadeReferralGlobalState.fetch(this) } + /** + * Fetch all the referral partners + */ + async getReferralPartners(): Promise { + const accounts = await this.config.connection.getProgramAccounts( + new web3.PublicKey(this.config.marinadeReferralProgramId), + { + filters: [ + { + dataSize: + this.marinadeReferralProgram.program.account.referralState.size + + 20 + + 96, // number of bytes, + }, + ], + } + ) + const codes = accounts.map((acc) => acc.pubkey) + return await Promise.all( + codes.map((referralCode) => this.getReferralPartnerState(referralCode)) + ) + } + /** * Returns a transaction with the instructions to * Add liquidity to the liquidity pool and receive LP tokens @@ -74,27 +107,36 @@ export class Marinade { * @param {BN} amountLamports - The amount of lamports added to the liquidity pool */ async addLiquidity(amountLamports: BN): Promise { - const ownerAddress = assertNotNullAndReturn(this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) + const ownerAddress = assertNotNullAndReturn( + this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) const marinadeState = await this.getMarinadeState() const transaction = new web3.Transaction() const { associatedTokenAccountAddress: associatedLPTokenAccountAddress, createAssociateTokenInstruction, - } = await getOrCreateAssociatedTokenAccount(this.provider, marinadeState.lpMintAddress, ownerAddress) + } = await getOrCreateAssociatedTokenAccount( + this.provider, + marinadeState.lpMintAddress, + ownerAddress + ) if (createAssociateTokenInstruction) { transaction.add(createAssociateTokenInstruction) } - const addLiquidityInstruction = this.marinadeFinanceProgram.addLiquidityInstruction({ - amountLamports, - accounts: await this.marinadeFinanceProgram.addLiquidityInstructionAccounts({ - marinadeState, - associatedLPTokenAccountAddress, - ownerAddress, - }), - }) + const addLiquidityInstruction = + this.marinadeFinanceProgram.addLiquidityInstruction({ + amountLamports, + accounts: + await this.marinadeFinanceProgram.addLiquidityInstructionAccounts({ + marinadeState, + associatedLPTokenAccountAddress, + ownerAddress, + }), + }) transaction.add(addLiquidityInstruction) @@ -110,31 +152,46 @@ export class Marinade { * * @param {BN} amountLamports - The amount of LP tokens burned */ - async removeLiquidity(amountLamports: BN): Promise { - const ownerAddress = assertNotNullAndReturn(this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) + async removeLiquidity( + amountLamports: BN + ): Promise { + const ownerAddress = assertNotNullAndReturn( + this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) const marinadeState = await this.getMarinadeState() const transaction = new web3.Transaction() - const associatedLPTokenAccountAddress = await getAssociatedTokenAccountAddress(marinadeState.lpMintAddress, ownerAddress) + const associatedLPTokenAccountAddress = + await getAssociatedTokenAccountAddress( + marinadeState.lpMintAddress, + ownerAddress + ) const { associatedTokenAccountAddress: associatedMSolTokenAccountAddress, createAssociateTokenInstruction, - } = await getOrCreateAssociatedTokenAccount(this.provider, marinadeState.mSolMintAddress, ownerAddress) + } = await getOrCreateAssociatedTokenAccount( + this.provider, + marinadeState.mSolMintAddress, + ownerAddress + ) if (createAssociateTokenInstruction) { transaction.add(createAssociateTokenInstruction) } - const removeLiquidityInstruction = this.marinadeFinanceProgram.removeLiquidityInstruction({ - amountLamports, - accounts: await this.marinadeFinanceProgram.removeLiquidityInstructionAccounts({ - marinadeState, - ownerAddress, - associatedLPTokenAccountAddress, - associatedMSolTokenAccountAddress, - }), - }) + const removeLiquidityInstruction = + this.marinadeFinanceProgram.removeLiquidityInstruction({ + amountLamports, + accounts: + await this.marinadeFinanceProgram.removeLiquidityInstructionAccounts({ + marinadeState, + ownerAddress, + associatedLPTokenAccountAddress, + associatedMSolTokenAccountAddress, + }), + }) transaction.add(removeLiquidityInstruction) @@ -152,16 +209,30 @@ export class Marinade { * @param {BN} amountLamports - The amount lamports staked * @param {DepositOptions=} options - Additional deposit options */ - async deposit(amountLamports: BN, options: DepositOptions = {}): Promise { - const feePayer = assertNotNullAndReturn(this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) - const mintToOwnerAddress = assertNotNullAndReturn(options.mintToOwnerAddress ?? this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) + async deposit( + amountLamports: BN, + options: DepositOptions = {} + ): Promise { + const feePayer = assertNotNullAndReturn( + this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) + const mintToOwnerAddress = assertNotNullAndReturn( + options.mintToOwnerAddress ?? this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) const marinadeState = await this.getMarinadeState() const transaction = new web3.Transaction() const { associatedTokenAccountAddress: associatedMSolTokenAccountAddress, createAssociateTokenInstruction, - } = await getOrCreateAssociatedTokenAccount(this.provider, marinadeState.mSolMintAddress, mintToOwnerAddress, feePayer) + } = await getOrCreateAssociatedTokenAccount( + this.provider, + marinadeState.mSolMintAddress, + mintToOwnerAddress, + feePayer + ) if (createAssociateTokenInstruction) { transaction.add(createAssociateTokenInstruction) @@ -189,15 +260,28 @@ export class Marinade { * * @param {BN} amountLamports - The amount of mSOL exchanged for SOL */ - async liquidUnstake(amountLamports: BN, associatedMSolTokenAccountAddress?: web3.PublicKey): Promise { - const ownerAddress = assertNotNullAndReturn(this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) + async liquidUnstake( + amountLamports: BN, + associatedMSolTokenAccountAddress?: web3.PublicKey + ): Promise { + const ownerAddress = assertNotNullAndReturn( + this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) const marinadeState = await this.getMarinadeState() const transaction = new web3.Transaction() if (!associatedMSolTokenAccountAddress) { - const associatedTokenAccountInfos = await getOrCreateAssociatedTokenAccount(this.provider, marinadeState.mSolMintAddress, ownerAddress) - const createAssociateTokenInstruction = associatedTokenAccountInfos.createAssociateTokenInstruction - associatedMSolTokenAccountAddress = associatedTokenAccountInfos.associatedTokenAccountAddress + const associatedTokenAccountInfos = + await getOrCreateAssociatedTokenAccount( + this.provider, + marinadeState.mSolMintAddress, + ownerAddress + ) + const createAssociateTokenInstruction = + associatedTokenAccountInfos.createAssociateTokenInstruction + associatedMSolTokenAccountAddress = + associatedTokenAccountInfos.associatedTokenAccountAddress if (createAssociateTokenInstruction) { transaction.add(createAssociateTokenInstruction) @@ -205,12 +289,13 @@ export class Marinade { } const program = this.provideReferralOrMainProgram() - const liquidUnstakeInstruction = await program.liquidUnstakeInstructionBuilder({ - amountLamports, - marinadeState, - ownerAddress, - associatedMSolTokenAccountAddress, - }) + const liquidUnstakeInstruction = + await program.liquidUnstakeInstructionBuilder({ + amountLamports, + marinadeState, + ownerAddress, + associatedMSolTokenAccountAddress, + }) transaction.add(liquidUnstakeInstruction) @@ -227,58 +312,85 @@ export class Marinade { * * @param {web3.PublicKey} stakeAccountAddress - The account to be deposited */ - async depositStakeAccount(stakeAccountAddress: web3.PublicKey): Promise { - const ownerAddress = assertNotNullAndReturn(this.config.publicKey, ErrorMessage.NO_PUBLIC_KEY) + async depositStakeAccount( + stakeAccountAddress: web3.PublicKey + ): Promise { + const ownerAddress = assertNotNullAndReturn( + this.config.publicKey, + ErrorMessage.NO_PUBLIC_KEY + ) const marinadeState = await this.getMarinadeState() const transaction = new web3.Transaction() const currentEpoch = await this.provider.connection.getEpochInfo() - const stakeAccountInfo = await getParsedStakeAccountInfo(this.provider, stakeAccountAddress) + const stakeAccountInfo = await getParsedStakeAccountInfo( + this.provider, + stakeAccountAddress + ) - const { authorizedWithdrawerAddress, voterAddress, activationEpoch, isCoolingDown } = stakeAccountInfo + const { + authorizedWithdrawerAddress, + voterAddress, + activationEpoch, + isCoolingDown, + } = stakeAccountInfo if (!authorizedWithdrawerAddress) { - throw new Error('Withdrawer address is not available!') + throw new Error("Withdrawer address is not available!") } if (!activationEpoch || !voterAddress) { - throw new Error('The stake account is not delegated!') + throw new Error("The stake account is not delegated!") } if (isCoolingDown) { - throw new Error('The stake is cooling down!') + throw new Error("The stake is cooling down!") } const waitEpochs = 2 const earliestDepositEpoch = activationEpoch.addn(waitEpochs) if (earliestDepositEpoch.gtn(currentEpoch.epoch)) { - throw new Error(`Deposited stake ${stakeAccountAddress} is not activated yet. Wait for #${earliestDepositEpoch} epoch`) + throw new Error( + `Deposited stake ${stakeAccountAddress} is not activated yet. Wait for #${earliestDepositEpoch} epoch` + ) } const { validatorRecords } = await marinadeState.getValidatorRecords() - const validatorLookupIndex = validatorRecords.findIndex(({ validatorAccount }) => validatorAccount.equals(voterAddress)) - const validatorIndex = validatorLookupIndex === -1 ? marinadeState.state.validatorSystem.validatorList.count : validatorLookupIndex - - const duplicationFlag = await marinadeState.validatorDuplicationFlag(voterAddress) + const validatorLookupIndex = validatorRecords.findIndex( + ({ validatorAccount }) => validatorAccount.equals(voterAddress) + ) + const validatorIndex = + validatorLookupIndex === -1 + ? marinadeState.state.validatorSystem.validatorList.count + : validatorLookupIndex + + const duplicationFlag = await marinadeState.validatorDuplicationFlag( + voterAddress + ) const { associatedTokenAccountAddress: associatedMSolTokenAccountAddress, createAssociateTokenInstruction, - } = await getOrCreateAssociatedTokenAccount(this.provider, marinadeState.mSolMintAddress, ownerAddress) + } = await getOrCreateAssociatedTokenAccount( + this.provider, + marinadeState.mSolMintAddress, + ownerAddress + ) if (createAssociateTokenInstruction) { transaction.add(createAssociateTokenInstruction) } const program = this.provideReferralOrMainProgram() - const depositStakeAccountInstruction = await program.depositStakeAccountInstructionBuilder({ - validatorIndex, - marinadeState, - duplicationFlag, - authorizedWithdrawerAddress, - associatedMSolTokenAccountAddress, - ownerAddress, - stakeAccountAddress, - }) + const depositStakeAccountInstruction = + await program.depositStakeAccountInstructionBuilder({ + validatorIndex, + marinadeState, + duplicationFlag, + authorizedWithdrawerAddress, + associatedMSolTokenAccountAddress, + ownerAddress, + stakeAccountAddress, + }) transaction.add(depositStakeAccountInstruction) @@ -299,24 +411,46 @@ export class Marinade { * @param {web3.PublicKey} stakeAccountAddress - The account to be deposited * @param {BN} mSolToKeep - Optional amount of mSOL lamports to keep */ - async liquidateStakeAccount(stakeAccountAddress: web3.PublicKey, mSolToKeep?: BN): Promise { - const totalBalance = await this.provider.connection.getBalance(stakeAccountAddress) - const rent = await this.provider.connection.getMinimumBalanceForRentExemption(web3.StakeProgram.space) + async liquidateStakeAccount( + stakeAccountAddress: web3.PublicKey, + mSolToKeep?: BN + ): Promise { + const totalBalance = await this.provider.connection.getBalance( + stakeAccountAddress + ) + const rent = + await this.provider.connection.getMinimumBalanceForRentExemption( + web3.StakeProgram.space + ) const stakeBalance = new BN(totalBalance - rent) const marinadeState = await this.getMarinadeState() - const { transaction: depositTx, associatedMSolTokenAccountAddress, voterAddress } = - await this.depositStakeAccount(stakeAccountAddress) + const { + transaction: depositTx, + associatedMSolTokenAccountAddress, + voterAddress, + } = await this.depositStakeAccount(stakeAccountAddress) let mSolAmountToReceive = computeMsolAmount(stakeBalance, marinadeState) // when working with referral partner the costs of the deposit operation is subtracted from the mSOL amount the user receives if (this.isReferralProgram()) { - const partnerOperationFee = (await this.marinadeReferralProgram.getReferralStateData()).operationDepositStakeAccountFee - mSolAmountToReceive = mSolAmountToReceive.sub(proportionalBN(mSolAmountToReceive, new BN(partnerOperationFee), new BN(10_000))) + const partnerOperationFee = ( + await this.marinadeReferralProgram.getReferralStateData() + ).operationDepositStakeAccountFee + mSolAmountToReceive = mSolAmountToReceive.sub( + proportionalBN( + mSolAmountToReceive, + new BN(partnerOperationFee), + new BN(10_000) + ) + ) } const unstakeAmountMSol = mSolAmountToReceive.sub(mSolToKeep ?? new BN(0)) - const { transaction: unstakeTx } = await this.liquidUnstake(unstakeAmountMSol, associatedMSolTokenAccountAddress) + const { transaction: unstakeTx } = await this.liquidUnstake( + unstakeAmountMSol, + associatedMSolTokenAccountAddress + ) return { transaction: depositTx.add(unstakeTx), @@ -328,8 +462,9 @@ export class Marinade { /** * @todo */ - async getDelayedUnstakeTickets(beneficiary?: web3.PublicKey): Promise> { - + async getDelayedUnstakeTickets( + beneficiary?: web3.PublicKey + ): Promise> { return this.marinadeFinanceProgram.getDelayedUnstakeTickets(beneficiary) } @@ -339,6 +474,8 @@ export class Marinade { */ async getEstimatedUnstakeTicketDueDate() { const marinadeState = await this.getMarinadeState() - return this.marinadeFinanceProgram.getEstimatedUnstakeTicketDueDate(marinadeState) + return this.marinadeFinanceProgram.getEstimatedUnstakeTicketDueDate( + marinadeState + ) } } diff --git a/test/marinade-referral-state.spec.ts b/test/marinade-referral-state.spec.ts index e37c9e4..8214ff7 100644 --- a/test/marinade-referral-state.spec.ts +++ b/test/marinade-referral-state.spec.ts @@ -1,10 +1,10 @@ -import { Marinade, MarinadeConfig, web3, BN } from '../src' -import { MarinadeReferralStateResponse } from '../src/marinade-referral-state/marinade-referral-state.types' -import * as TestWorld from './test-world' +import { Marinade, MarinadeConfig, web3, BN } from "../src" +import { MarinadeReferralStateResponse } from "../src/marinade-referral-state/marinade-referral-state.types" +import * as TestWorld from "./test-world" -describe('Marinade Referral Program', () => { - describe('getReferralGlobalState', () => { - it('fetches the referral program\'s global state which matches the expected type', async() => { +describe("Marinade Referral Program", () => { + describe("getReferralGlobalState", () => { + it("fetches the referral program's global state which matches the expected type", async() => { const config = new MarinadeConfig({ connection: TestWorld.CONNECTION, referralCode: TestWorld.REFERRAL_CODE, @@ -24,8 +24,8 @@ describe('Marinade Referral Program', () => { }) }) - describe('getReferralPartnerState', () => { - it('fetches the referral partner\' state which matches the expected type', async() => { + describe("getReferralPartnerState", () => { + it("fetches the referral partner' state which matches the expected type", async() => { const config = new MarinadeConfig({ connection: TestWorld.CONNECTION, referralCode: TestWorld.REFERRAL_CODE, @@ -65,4 +65,60 @@ describe('Marinade Referral Program', () => { }) }) }) + + describe("getReferralPartnerState", () => { + it("fetches the referral partner' state using argument and no config and which matches the expected type", async() => { + const config = new MarinadeConfig({ + connection: TestWorld.CONNECTION, + }) + const marinade = new Marinade(config) + + const { state } = await marinade.getReferralPartnerState( + TestWorld.REFERRAL_CODE + ) + + expect(state).toStrictEqual({ + baseFee: expect.any(Number), + validatorVoteKey: null, + keepSelfStakePct: expect.any(Number), + delayedUnstakeAmount: expect.any(BN), + delayedUnstakeOperations: expect.any(BN), + depositSolAmount: expect.any(BN), + depositSolOperations: expect.any(BN), + depositStakeAccountAmount: expect.any(BN), + depositStakeAccountOperations: expect.any(BN), + liqUnstakeSolAmount: expect.any(BN), + liqUnstakeMsolAmount: expect.any(BN), + liqUnstakeMsolFees: expect.any(BN), + liqUnstakeOperations: expect.any(BN), + maxFee: expect.any(Number), + maxNetStake: expect.any(BN), + partnerAccount: expect.any(web3.PublicKey), + partnerName: TestWorld.PARTNER_NAME, + pause: expect.any(Boolean), + msolTokenPartnerAccount: expect.any(web3.PublicKey), + operationDepositSolFee: expect.any(Number), + operationDepositStakeAccountFee: expect.any(Number), + operationLiquidUnstakeFee: expect.any(Number), + operationDelayedUnstakeFee: expect.any(Number), + accumDepositSolFee: expect.any(BN), + accumDepositStakeAccountFee: expect.any(BN), + accumLiquidUnstakeFee: expect.any(BN), + accumDelayedUnstakeFee: expect.any(BN), + }) + }) + }) + + describe("getReferralPartners", () => { + it("fetches all the referral partners ", async() => { + const config = new MarinadeConfig({ + connection: TestWorld.CONNECTION, + }) + const marinade = new Marinade(config) + + const partners = await marinade.getReferralPartners() + + expect(partners.length).toBeGreaterThan(0) + }) + }) })