From eb48544fe6d3ecd01e3dbe5160c9997dd86d7919 Mon Sep 17 00:00:00 2001 From: Jeremy Bogle Date: Tue, 29 Nov 2022 16:59:29 -0500 Subject: [PATCH] Reset stake entry (#27) --- README.md | 6 - programs/cardinal-rewards-center/src/lib.rs | 3 + .../src/stake_entry/mod.rs | 9 +- .../src/stake_entry/reset_stake_entry.rs | 23 ++ sdk/generated/instructions/index.ts | 1 + sdk/generated/instructions/resetStakeEntry.ts | 81 +++++++ sdk/idl/cardinal_rewards_center.ts | 42 ++++ sdk/idl/cardinal_rewards_center_idl.json | 21 ++ tests/stake-entry/stake-reset-entry.test.ts | 205 ++++++++++++++++++ tests/stake-entry/stake-unstake-reset.test.ts | 2 +- tests/stake-entry/stake-unstake.test.ts | 2 +- .../stake-pool/init-update-stake-pool.test.ts | 2 +- 12 files changed, 385 insertions(+), 12 deletions(-) create mode 100644 programs/cardinal-rewards-center/src/stake_entry/reset_stake_entry.rs create mode 100644 sdk/generated/instructions/resetStakeEntry.ts create mode 100644 tests/stake-entry/stake-reset-entry.test.ts diff --git a/README.md b/README.md index 9c05f317..c76fb990 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,3 @@ If you are developing using Cardinal rewards-center contracts and libraries, fee For issues please, file a GitHub issue. > https://discord.gg/cardinallabs - -## License - -Cardinal Protocol is licensed under the GNU Affero General Public License v3.0. - -In short, this means that any changes to this code must be made open source and available under the AGPL-v3.0 license, even if only used privately. diff --git a/programs/cardinal-rewards-center/src/lib.rs b/programs/cardinal-rewards-center/src/lib.rs index 7f3bfe38..442df635 100644 --- a/programs/cardinal-rewards-center/src/lib.rs +++ b/programs/cardinal-rewards-center/src/lib.rs @@ -42,6 +42,9 @@ pub mod cardinal_rewards_center { pub fn update_total_stake_seconds(ctx: Context) -> Result<()> { stake_entry::update_total_stake_seconds::handler(ctx) } + pub fn reset_stake_entry(ctx: Context) -> Result<()> { + stake_entry::reset_stake_entry::handler(ctx) + } pub fn close_stake_entry(ctx: Context) -> Result<()> { stake_entry::close_stake_entry::handler(ctx) } diff --git a/programs/cardinal-rewards-center/src/stake_entry/mod.rs b/programs/cardinal-rewards-center/src/stake_entry/mod.rs index 65fb8048..74a22261 100644 --- a/programs/cardinal-rewards-center/src/stake_entry/mod.rs +++ b/programs/cardinal-rewards-center/src/stake_entry/mod.rs @@ -4,12 +4,15 @@ pub use state::*; pub mod init_entry; pub use init_entry::*; -pub mod close_stake_entry; -pub use close_stake_entry::*; - pub mod update_total_stake_seconds; pub use update_total_stake_seconds::*; +pub mod reset_stake_entry; +pub use reset_stake_entry::*; + +pub mod close_stake_entry; +pub use close_stake_entry::*; + pub mod editions; pub use editions::stake_edition::*; pub use editions::unstake_edition::*; diff --git a/programs/cardinal-rewards-center/src/stake_entry/reset_stake_entry.rs b/programs/cardinal-rewards-center/src/stake_entry/reset_stake_entry.rs new file mode 100644 index 00000000..e419c054 --- /dev/null +++ b/programs/cardinal-rewards-center/src/stake_entry/reset_stake_entry.rs @@ -0,0 +1,23 @@ +use crate::errors::ErrorCode; +use crate::StakeEntry; +use crate::StakePool; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ResetStakeEntryCtx<'info> { + stake_pool: Box>, + #[account(mut, constraint = stake_pool.key() == stake_entry.pool @ ErrorCode::InvalidStakePool)] + stake_entry: Box>, + #[account(mut, constraint = stake_pool.authority == authority.key() @ ErrorCode::InvalidAuthority)] + authority: Signer<'info>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let stake_entry = &mut ctx.accounts.stake_entry; + stake_entry.total_stake_seconds = 0; + stake_entry.used_stake_seconds = 0; + stake_entry.last_updated_at = Clock::get().unwrap().unix_timestamp; + stake_entry.last_staked_at = Clock::get().unwrap().unix_timestamp; + stake_entry.cooldown_start_seconds = None; + Ok(()) +} diff --git a/sdk/generated/instructions/index.ts b/sdk/generated/instructions/index.ts index 70c35ae0..7ca91956 100644 --- a/sdk/generated/instructions/index.ts +++ b/sdk/generated/instructions/index.ts @@ -19,6 +19,7 @@ export * from './initRewardDistributor' export * from './initRewardEntry' export * from './initRewardReceipt' export * from './initStakeBooster' +export * from './resetStakeEntry' export * from './setRewardReceiptAllowed' export * from './stakeEdition' export * from './unstakeEdition' diff --git a/sdk/generated/instructions/resetStakeEntry.ts b/sdk/generated/instructions/resetStakeEntry.ts new file mode 100644 index 00000000..95047abc --- /dev/null +++ b/sdk/generated/instructions/resetStakeEntry.ts @@ -0,0 +1,81 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category ResetStakeEntry + * @category generated + */ +export const resetStakeEntryStruct = new beet.BeetArgsStruct<{ + instructionDiscriminator: number[] /* size: 8 */ +}>( + [['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)]], + 'ResetStakeEntryInstructionArgs' +) +/** + * Accounts required by the _resetStakeEntry_ instruction + * + * @property [] stakePool + * @property [_writable_] stakeEntry + * @property [_writable_, **signer**] authority + * @category Instructions + * @category ResetStakeEntry + * @category generated + */ +export type ResetStakeEntryInstructionAccounts = { + stakePool: web3.PublicKey + stakeEntry: web3.PublicKey + authority: web3.PublicKey +} + +export const resetStakeEntryInstructionDiscriminator = [ + 189, 90, 39, 72, 82, 90, 236, 109, +] + +/** + * Creates a _ResetStakeEntry_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @category Instructions + * @category ResetStakeEntry + * @category generated + */ +export function createResetStakeEntryInstruction( + accounts: ResetStakeEntryInstructionAccounts, + programId = new web3.PublicKey('rwcn6Ry17ChPXpJCN2hoK5kwpgFarQqzycXwVJ3om7U') +) { + const [data] = resetStakeEntryStruct.serialize({ + instructionDiscriminator: resetStakeEntryInstructionDiscriminator, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.stakePool, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.stakeEntry, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.authority, + isWritable: true, + isSigner: true, + }, + ] + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/idl/cardinal_rewards_center.ts b/sdk/idl/cardinal_rewards_center.ts index b5f8124a..71b68f60 100644 --- a/sdk/idl/cardinal_rewards_center.ts +++ b/sdk/idl/cardinal_rewards_center.ts @@ -136,6 +136,27 @@ export type CardinalRewardsCenter = { ]; args: []; }, + { + name: "resetStakeEntry"; + accounts: [ + { + name: "stakePool"; + isMut: false; + isSigner: false; + }, + { + name: "stakeEntry"; + isMut: true; + isSigner: false; + }, + { + name: "authority"; + isMut: true; + isSigner: true; + } + ]; + args: []; + }, { name: "closeStakeEntry"; accounts: [ @@ -2290,6 +2311,27 @@ export const IDL: CardinalRewardsCenter = { ], args: [], }, + { + name: "resetStakeEntry", + accounts: [ + { + name: "stakePool", + isMut: false, + isSigner: false, + }, + { + name: "stakeEntry", + isMut: true, + isSigner: false, + }, + { + name: "authority", + isMut: true, + isSigner: true, + }, + ], + args: [], + }, { name: "closeStakeEntry", accounts: [ diff --git a/sdk/idl/cardinal_rewards_center_idl.json b/sdk/idl/cardinal_rewards_center_idl.json index ec04ffa5..bf4a5936 100644 --- a/sdk/idl/cardinal_rewards_center_idl.json +++ b/sdk/idl/cardinal_rewards_center_idl.json @@ -136,6 +136,27 @@ ], "args": [] }, + { + "name": "resetStakeEntry", + "accounts": [ + { + "name": "stakePool", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeEntry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [] + }, { "name": "closeStakeEntry", "accounts": [ diff --git a/tests/stake-entry/stake-reset-entry.test.ts b/tests/stake-entry/stake-reset-entry.test.ts new file mode 100644 index 00000000..809041b6 --- /dev/null +++ b/tests/stake-entry/stake-reset-entry.test.ts @@ -0,0 +1,205 @@ +import { beforeAll, expect, test } from "@jest/globals"; +import { Wallet } from "@project-serum/anchor"; +import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import type { PublicKey } from "@solana/web3.js"; +import { Keypair, Transaction } from "@solana/web3.js"; + +import { + findStakeEntryId, + findStakePoolId, + SOL_PAYMENT_INFO, + stake, +} from "../../sdk"; +import { + createInitPoolInstruction, + createResetStakeEntryInstruction, + StakeEntry, + StakePool, +} from "../../sdk/generated"; +import type { CardinalProvider } from "../utils"; +import { + createMasterEditionTx, + executeTransaction, + executeTransactions, + getProvider, + newAccountWithLamports, +} from "../utils"; + +const stakePoolIdentifier = `test-${Math.random()}`; +let provider: CardinalProvider; +let mintId: PublicKey; +let nonAuthority: Keypair; + +beforeAll(async () => { + provider = await getProvider(); + const mintKeypair = Keypair.generate(); + mintId = mintKeypair.publicKey; + nonAuthority = await newAccountWithLamports(provider.connection); + await executeTransaction( + provider.connection, + await createMasterEditionTx( + provider.connection, + mintKeypair.publicKey, + provider.wallet.publicKey + ), + provider.wallet, + { signers: [mintKeypair] } + ); +}); + +test("Init pool", async () => { + const tx = new Transaction(); + const stakePoolId = findStakePoolId(stakePoolIdentifier); + tx.add( + createInitPoolInstruction( + { + stakePool: stakePoolId, + payer: provider.wallet.publicKey, + }, + { + ix: { + identifier: stakePoolIdentifier, + allowedCollections: [], + allowedCreators: [], + requiresAuthorization: false, + authority: provider.wallet.publicKey, + resetOnUnstake: false, + cooldownSeconds: null, + minStakeSeconds: null, + endDate: null, + stakePaymentInfo: SOL_PAYMENT_INFO, + unstakePaymentInfo: SOL_PAYMENT_INFO, + }, + } + ) + ); + await executeTransaction(provider.connection, tx, provider.wallet); + const pool = await StakePool.fromAccountAddress( + provider.connection, + stakePoolId + ); + expect(pool.authority.toString()).toBe(provider.wallet.publicKey.toString()); + expect(pool.requiresAuthorization).toBe(false); +}); + +test("Stake", async () => { + await executeTransactions( + provider.connection, + await stake(provider.connection, provider.wallet, stakePoolIdentifier, [ + { mintId }, + ]), + provider.wallet + ); + + const stakePoolId = findStakePoolId(stakePoolIdentifier); + const stakeEntryId = findStakeEntryId(stakePoolId, mintId); + const userAtaId = getAssociatedTokenAddressSync( + mintId, + provider.wallet.publicKey + ); + const entry = await StakeEntry.fromAccountAddress( + provider.connection, + stakeEntryId + ); + expect(entry.stakeMint.toString()).toBe(mintId.toString()); + expect(entry.lastStaker.toString()).toBe( + provider.wallet.publicKey.toString() + ); + expect(parseInt(entry.lastStakedAt.toString())).toBeGreaterThan( + Date.now() / 1000 - 60 + ); + expect(parseInt(entry.lastUpdatedAt.toString())).toBeGreaterThan( + Date.now() / 1000 - 60 + ); + + const userAta = await getAccount(provider.connection, userAtaId); + expect(userAta.isFrozen).toBe(true); + expect(parseInt(userAta.amount.toString())).toBe(1); + const activeStakeEntries = await StakeEntry.gpaBuilder() + .addFilter("lastStaker", provider.wallet.publicKey) + .run(provider.connection); + expect(activeStakeEntries.length).toBe(1); +}); + +test("Stake again fail", async () => { + await expect( + executeTransactions( + provider.connection, + await stake(provider.connection, provider.wallet, stakePoolIdentifier, [ + { mintId }, + ]), + provider.wallet, + { silent: true } + ) + ).rejects.toThrow(); +}); + +test("Reset fail", async () => { + const stakePoolId = findStakePoolId(stakePoolIdentifier); + const stakeEntryId = findStakeEntryId(stakePoolId, mintId); + await expect( + executeTransaction( + provider.connection, + new Transaction().add( + createResetStakeEntryInstruction({ + stakePool: stakePoolId, + stakeEntry: stakeEntryId, + authority: nonAuthority.publicKey, + }) + ), + new Wallet(nonAuthority), + { silent: true } + ) + ).rejects.toThrow(); +}); + +test("Reset stake entry", async () => { + await new Promise((r) => setTimeout(r, 2000)); + const stakePoolId = findStakePoolId(stakePoolIdentifier); + const stakeEntryId = findStakeEntryId(stakePoolId, mintId); + const stakeEntry = await StakeEntry.fromAccountAddress( + provider.connection, + stakeEntryId + ); + + await executeTransaction( + provider.connection, + new Transaction().add( + createResetStakeEntryInstruction({ + stakePool: stakePoolId, + stakeEntry: stakeEntryId, + authority: provider.wallet.publicKey, + }) + ), + provider.wallet, + { silent: true } + ); + + const userAtaId = getAssociatedTokenAddressSync( + mintId, + provider.wallet.publicKey + ); + const checkEntry = await StakeEntry.fromAccountAddress( + provider.connection, + stakeEntryId + ); + expect(checkEntry.stakeMint.toString()).toBe(mintId.toString()); + expect(checkEntry.lastStaker.toString()).toBe( + provider.wallet.publicKey.toString() + ); + expect(parseInt(checkEntry.lastStakedAt.toString())).toBeGreaterThan( + parseInt(stakeEntry.lastStakedAt.toString()) + ); + expect(parseInt(checkEntry.lastUpdatedAt.toString())).toBeGreaterThan( + parseInt(stakeEntry.lastUpdatedAt.toString()) + ); + expect(checkEntry.cooldownStartSeconds).toBe(null); + + const userAta = await getAccount(provider.connection, userAtaId); + expect(userAta.isFrozen).toBe(true); + expect(parseInt(userAta.amount.toString())).toBe(1); + const activeStakeEntries = await StakeEntry.gpaBuilder() + .addFilter("lastStaker", provider.wallet.publicKey) + .run(provider.connection); + expect(activeStakeEntries.length).toBe(1); +}); diff --git a/tests/stake-entry/stake-unstake-reset.test.ts b/tests/stake-entry/stake-unstake-reset.test.ts index 5d7ea36f..2cc13234 100644 --- a/tests/stake-entry/stake-unstake-reset.test.ts +++ b/tests/stake-entry/stake-unstake-reset.test.ts @@ -4,9 +4,9 @@ import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import * as tokenMetadatV1 from "mpl-token-metadata-v1"; import { - SOL_PAYMENT_INFO, findStakeEntryId, findStakePoolId, + SOL_PAYMENT_INFO, stake, unstake, } from "../../sdk"; diff --git a/tests/stake-entry/stake-unstake.test.ts b/tests/stake-entry/stake-unstake.test.ts index 595da05c..ff84744e 100644 --- a/tests/stake-entry/stake-unstake.test.ts +++ b/tests/stake-entry/stake-unstake.test.ts @@ -3,9 +3,9 @@ import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { - SOL_PAYMENT_INFO, findStakeEntryId, findStakePoolId, + SOL_PAYMENT_INFO, stake, unstake, } from "../../sdk"; diff --git a/tests/stake-pool/init-update-stake-pool.test.ts b/tests/stake-pool/init-update-stake-pool.test.ts index b27ac99e..8ea0328c 100644 --- a/tests/stake-pool/init-update-stake-pool.test.ts +++ b/tests/stake-pool/init-update-stake-pool.test.ts @@ -1,7 +1,7 @@ import { beforeAll, expect, test } from "@jest/globals"; import { Transaction } from "@solana/web3.js"; -import { SOL_PAYMENT_INFO, findStakePoolId } from "../../sdk"; +import { findStakePoolId, SOL_PAYMENT_INFO } from "../../sdk"; import { createInitPoolInstruction, createUpdatePoolInstruction,