From a9bffed1e68738918578d733a03db50d26ea2ab0 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 15 Sep 2025 18:13:02 +0200 Subject: [PATCH 01/29] feat: configurable fees receiver --- src/args/mod.rs | 2 + src/args/set_fees_receiver.rs | 7 + src/discriminator.rs | 3 + src/instruction_builder/mod.rs | 2 + .../protocol_claim_fees.rs | 8 +- src/instruction_builder/set_fees_receiver.rs | 32 +++ src/lib.rs | 3 + src/processor/mod.rs | 2 + src/processor/protocol_claim_fees.rs | 24 +- src/processor/set_fees_receiver.rs | 92 ++++++++ .../whitelist_validator_for_program.rs | 5 +- src/state/program_config.rs | 3 +- tests/fixtures/accounts.rs | 3 +- tests/integration/Cargo.lock | 16 +- tests/integration/package-lock.json | 128 ++--------- tests/integration/package.json | 4 +- .../programs/test-delegation/Cargo.toml | 2 +- .../programs/test-delegation/src/lib.rs | 19 +- tests/integration/tests/test-delegation.ts | 94 ++++++-- tests/integration/yarn.lock | 149 ++++++------- .../test_commit_state_with_program_config.rs | 13 +- tests/test_protocol_claim_fees.rs | 19 +- tests/test_set_fees_receiver.rs | 208 ++++++++++++++++++ 23 files changed, 603 insertions(+), 235 deletions(-) create mode 100644 src/args/set_fees_receiver.rs create mode 100644 src/instruction_builder/set_fees_receiver.rs create mode 100644 src/processor/set_fees_receiver.rs create mode 100644 tests/test_set_fees_receiver.rs diff --git a/src/args/mod.rs b/src/args/mod.rs index 456d44d7..f79e1b83 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -2,6 +2,7 @@ mod call_handler; mod commit_state; mod delegate; mod delegate_ephemeral_balance; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod validator_claim_fees; mod whitelist_validator_for_program; @@ -10,6 +11,7 @@ pub use call_handler::*; pub use commit_state::*; pub use delegate::*; pub use delegate_ephemeral_balance::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/args/set_fees_receiver.rs b/src/args/set_fees_receiver.rs new file mode 100644 index 00000000..70784343 --- /dev/null +++ b/src/args/set_fees_receiver.rs @@ -0,0 +1,7 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct SetFeesReceiverArgs { + pub fees_receiver: Pubkey, +} diff --git a/src/discriminator.rs b/src/discriminator.rs index 08b14b09..eef4941c 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -35,6 +35,8 @@ pub enum DlpDiscriminator { CloseValidatorFeesVault = 14, /// See [crate::processor::process_call_handler] for docs. CallHandler = 15, + /// See [crate::processor::process_set_fees_receiver] for docs. + SetFeesReceiver = 16, } impl DlpDiscriminator { @@ -63,6 +65,7 @@ impl TryFrom<[u8; 8]> for DlpDiscriminator { 0xd => Ok(DlpDiscriminator::CommitStateFromBuffer), 0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault), 0xf => Ok(DlpDiscriminator::CallHandler), + 0x10 => Ok(DlpDiscriminator::SetFeesReceiver), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 139f52de..bda59ef5 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -9,6 +9,7 @@ mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod undelegate; mod validator_claim_fees; @@ -25,6 +26,7 @@ pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; pub use validator_claim_fees::*; diff --git a/src/instruction_builder/protocol_claim_fees.rs b/src/instruction_builder/protocol_claim_fees.rs index 91753999..e687cc0f 100644 --- a/src/instruction_builder/protocol_claim_fees.rs +++ b/src/instruction_builder/protocol_claim_fees.rs @@ -2,12 +2,13 @@ use solana_program::instruction::Instruction; use solana_program::{bpf_loader_upgradeable, instruction::AccountMeta, pubkey::Pubkey}; use crate::discriminator::DlpDiscriminator; -use crate::pda::fees_vault_pda; +use crate::pda::{fees_vault_pda, program_config_from_program_id}; /// Claim the accrued fees from the protocol fees vault. /// See [crate::processor::process_protocol_claim_fees] for docs. -pub fn protocol_claim_fees(admin: Pubkey) -> Instruction { +pub fn protocol_claim_fees(admin: Pubkey, fees_receiver: Pubkey, program: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); + let program_config_pda = program_config_from_program_id(&program); let delegation_program_data = Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { @@ -15,6 +16,9 @@ pub fn protocol_claim_fees(admin: Pubkey) -> Instruction { accounts: vec![ AccountMeta::new(admin, true), AccountMeta::new(fees_vault_pda, false), + AccountMeta::new(program_config_pda, false), + AccountMeta::new(fees_receiver, false), + AccountMeta::new_readonly(program, false), AccountMeta::new_readonly(delegation_program_data, false), ], data: DlpDiscriminator::ProtocolClaimFees.to_vec(), diff --git a/src/instruction_builder/set_fees_receiver.rs b/src/instruction_builder/set_fees_receiver.rs new file mode 100644 index 00000000..439a893b --- /dev/null +++ b/src/instruction_builder/set_fees_receiver.rs @@ -0,0 +1,32 @@ +use borsh::to_vec; +use solana_program::instruction::Instruction; +use solana_program::{ + bpf_loader_upgradeable, instruction::AccountMeta, pubkey::Pubkey, system_program, +}; + +use crate::args::SetFeesReceiverArgs; +use crate::discriminator::DlpDiscriminator; +use crate::pda::program_config_from_program_id; + +/// Set the fees receiver. +/// See [crate::processor::process_set_fees_receiver] for docs. +pub fn set_fees_receiver(admin: Pubkey, fees_receiver: Pubkey, program: Pubkey) -> Instruction { + let program_config_pda = program_config_from_program_id(&program); + let delegation_program_data = + Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; + Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new(admin, true), + AccountMeta::new(program_config_pda, false), + AccountMeta::new_readonly(program, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(delegation_program_data, false), + ], + data: [ + DlpDiscriminator::SetFeesReceiver.to_vec(), + to_vec(&SetFeesReceiverArgs { fees_receiver }).unwrap(), + ] + .concat(), + } +} diff --git a/src/lib.rs b/src/lib.rs index 946aef92..55d386b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,9 @@ pub fn process_instruction( discriminator::DlpDiscriminator::CallHandler => { process_call_handler(program_id, accounts, data)? } + discriminator::DlpDiscriminator::SetFeesReceiver => { + processor::process_set_fees_receiver(program_id, accounts, data)? + } } Ok(()) } diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 9c4f9155..553c2851 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -9,6 +9,7 @@ mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; +mod set_fees_receiver; mod top_up_ephemeral_balance; mod undelegate; mod utils; @@ -26,6 +27,7 @@ pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; +pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; pub use undelegate::*; pub use validator_claim_fees::*; diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index d3aa17af..41cdbd9b 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -1,7 +1,9 @@ use crate::error::DlpError::Unauthorized; use crate::processor::utils::loaders::{ - load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, + load_account, load_initialized_protocol_fees_vault, load_program_config, + load_program_upgrade_authority, load_signer, }; +use crate::state::ProgramConfig; use solana_program::msg; use solana_program::program_error::ProgramError; use solana_program::rent::Rent; @@ -13,6 +15,9 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// /// 1. `[signer]` admin account that can claim the fees /// 2. `[writable]` protocol fees vault PDA +/// 3. `[]` program config PDA +/// 4. `[writable]` fees receiver PDA +/// 5. `[]` delegation program /// /// Requirements: /// @@ -28,13 +33,16 @@ pub fn process_protocol_claim_fees( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [admin, fees_vault, delegation_program_data] = accounts else { + let [admin, fees_vault, program_config_account, fees_receiver, program, delegation_program_data] = + accounts + else { return Err(ProgramError::NotEnoughAccountKeys); }; // Check if the admin is signer load_signer(admin, "admin")?; load_initialized_protocol_fees_vault(fees_vault, true)?; + load_program_config(program_config_account, *program.key, true)?; // Check if the admin is the correct one let admin_pubkey = @@ -48,6 +56,16 @@ pub fn process_protocol_claim_fees( return Err(Unauthorized.into()); } + let program_config_data = program_config_account.try_borrow_data()?; + let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)?; + + load_account( + fees_receiver, + program_config.fees_receiver, + true, + "fees receiver", + )?; + // Calculate the amount to transfer let min_rent = Rent::default().minimum_balance(8); if fees_vault.lamports() < min_rent { @@ -61,7 +79,7 @@ pub fn process_protocol_claim_fees( .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; - **admin.try_borrow_mut_lamports()? = admin + **fees_receiver.try_borrow_mut_lamports()? = fees_receiver .lamports() .checked_add(amount) .ok_or(ProgramError::ArithmeticOverflow)?; diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs new file mode 100644 index 00000000..1c773420 --- /dev/null +++ b/src/processor/set_fees_receiver.rs @@ -0,0 +1,92 @@ +use crate::args::SetFeesReceiverArgs; +use crate::error::DlpError::Unauthorized; +use crate::processor::utils::loaders::{ + load_program_config, load_program_upgrade_authority, load_signer, +}; +use crate::processor::utils::pda::resize_pda; +use crate::state::ProgramConfig; +use borsh::BorshDeserialize; +use solana_program::msg; +use solana_program::program_error::ProgramError; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +/// Process request to claim fees from the protocol fees vault +/// +/// Accounts: +/// +/// 1. `[signer, writable]` admin account that can set the fees receiver +/// 2. `[writable]` program config PDA +/// 3. `[]` program +/// 4. `[]` system program +/// +/// Requirements: +/// +/// - program config is initialized +/// - admin is the protocol config admin +/// +/// 1. Set the fees receiver in the protocol config +pub fn process_set_fees_receiver( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + // Load Accounts + let [admin, program_config_account, program, system_program, delegation_program_data] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Check if the admin is signer + load_signer(admin, "admin")?; + + // Check if the admin is the correct one + let admin_pubkey = + load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; + if !admin.key.eq(&admin_pubkey) { + msg!( + "Expected admin pubkey: {} but got {}", + admin_pubkey, + admin.key + ); + return Err(Unauthorized.into()); + } + + // Check if the program config is initialized + if !load_program_config(program_config_account, *program.key, true)? { + return Err(ProgramError::UninitializedAccount); + } + + // Migrate to the new account structure + let (mut program_config, migrated) = { + let program_config_data = program_config_account.try_borrow_data()?; + match ProgramConfig::try_from_bytes_with_discriminator(&program_config_data) { + Ok(program_config) => (program_config, false), + Err(_) => { + // Migrating the account + let mut data = program_config_data.to_vec(); + data.extend(Pubkey::default().to_bytes()); + let program_config = ProgramConfig::try_from_bytes_with_discriminator(&data)?; + + (program_config, true) + } + } + }; + + if migrated { + resize_pda( + admin, + program_config_account, + system_program, + program_config.size_with_discriminator(), + )?; + } + + let args = SetFeesReceiverArgs::try_from_slice(data)?; + program_config.fees_receiver = args.fees_receiver; + + let mut program_config_data = program_config_account.try_borrow_mut_data()?; + program_config.to_bytes_with_discriminator(&mut program_config_data.as_mut())?; + + Ok(()) +} diff --git a/src/processor/whitelist_validator_for_program.rs b/src/processor/whitelist_validator_for_program.rs index d0d51b3f..8e1fb67b 100644 --- a/src/processor/whitelist_validator_for_program.rs +++ b/src/processor/whitelist_validator_for_program.rs @@ -72,7 +72,10 @@ pub fn process_whitelist_validator_for_program( system_program, authority, )?; - ProgramConfig::default() + ProgramConfig { + approved_validators: Default::default(), + fees_receiver: *authority.key, + } } else { let program_config_data = program_config_account.try_borrow_data()?; ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)? diff --git a/src/state/program_config.rs b/src/state/program_config.rs index 21b4aff4..eab7f090 100644 --- a/src/state/program_config.rs +++ b/src/state/program_config.rs @@ -9,6 +9,7 @@ use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; #[derive(BorshSerialize, BorshDeserialize, Default, Debug)] pub struct ProgramConfig { pub approved_validators: BTreeSet, + pub fees_receiver: Pubkey, } impl AccountWithDiscriminator for ProgramConfig { @@ -19,7 +20,7 @@ impl AccountWithDiscriminator for ProgramConfig { impl ProgramConfig { pub fn size_with_discriminator(&self) -> usize { - 8 + 4 + 32 * self.approved_validators.len() + 8 + 4 + 32 * self.approved_validators.len() + 32 } } diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index 6a4c8d68..e2e78576 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -138,9 +138,10 @@ pub fn get_commit_record_account_data(authority: Pubkey) -> Vec { } #[allow(dead_code)] -pub fn create_program_config_data(approved_validator: Pubkey) -> Vec { +pub fn create_program_config_data(approved_validator: Pubkey, fees_receiver: Pubkey) -> Vec { let mut program_config = ProgramConfig { approved_validators: Default::default(), + fees_receiver, }; program_config .approved_validators diff --git a/tests/integration/Cargo.lock b/tests/integration/Cargo.lock index fa2b1927..9cdc153d 100644 --- a/tests/integration/Cargo.lock +++ b/tests/integration/Cargo.lock @@ -533,7 +533,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk" -version = "0.2.10" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060549fc24e8d948bf788e4b26a1afa24927ba3d1f02570324950c8d087d1bb4" dependencies = [ "anchor-lang", "borsh 1.5.7", @@ -545,7 +547,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-commit" -version = "0.2.10" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975c1979c2f23b729cc0e6122f9ffc93daad4cb211502c2eac3297b5a8874fb" dependencies = [ "quote", "syn 1.0.109", @@ -553,7 +557,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" -version = "0.2.10" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4905662d18fb332f1673419ad548a4c021c9a38270e9df768a16b34b055aee25" dependencies = [ "proc-macro2", "quote", @@ -562,7 +568,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" -version = "0.2.10" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7cb320b197c7efae7c65965bb4c025d5bd08d58cdc3523a7d61ac2266ba3f4" dependencies = [ "proc-macro2", "quote", diff --git a/tests/integration/package-lock.json b/tests/integration/package-lock.json index d60e3949..29c59778 100644 --- a/tests/integration/package-lock.json +++ b/tests/integration/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "integration", "dependencies": { "@coral-xyz/anchor": "0.31.1" }, @@ -87,7 +86,9 @@ "license": "MIT" }, "node_modules/@magicblock-labs/ephemeral-rollups-sdk": { - "version": "0.2.10", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.2.11.tgz", + "integrity": "sha512-I9pqYBsWhkUpndt9b4P6jqpVhAKaMdyyiBuxEX+Y2VdWeXuRpF0qXngyn26i1oKCVvbPNQYLu033LpNeSB4BKg==", "dev": true, "license": "MIT", "dependencies": { @@ -95,129 +96,41 @@ "@phala/dcap-qvl-web": "^0.2.7", "@solana/web3.js": "^1.98.0", "bs58": "^6.0.0", - "rpc-websockets": "^9.0.4" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/@phala/dcap-qvl-web": { - "version": "0.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/@swc/helpers": { - "version": "0.5.17", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/@types/node": { - "version": "24.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/@types/ws": { - "version": "8.18.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "rpc-websockets": "^9.0.4", + "typescript": "^5.3.0" } }, "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/base-x": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", "dev": true, "license": "MIT" }, "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/bs58": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", "dev": true, "license": "MIT", "dependencies": { "base-x": "^5.0.0" } }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/bufferutil": { - "version": "4.0.9", + "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/node-gyp-build": { - "version": "4.8.4", - "dev": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/rpc-websockets": { - "version": "9.1.3", - "dev": true, - "license": "LGPL-3.0-only", - "dependencies": { - "@swc/helpers": "^0.5.11", - "@types/uuid": "^8.3.4", - "@types/ws": "^8.2.2", - "buffer": "^6.0.3", - "eventemitter3": "^5.0.1", - "uuid": "^8.3.2", - "ws": "^8.5.0" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/kozjak" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "optionalDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - } - }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/rpc-websockets/node_modules/ws": { - "version": "8.18.3", - "dev": true, - "license": "MIT", "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=14.17" } }, - "node_modules/@magicblock-labs/ephemeral-rollups-sdk/node_modules/undici-types": { - "version": "7.10.0", - "dev": true, - "license": "MIT" - }, "node_modules/@metaplex-foundation/beet": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.2.tgz", @@ -300,6 +213,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@phala/dcap-qvl-web": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz", + "integrity": "sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", diff --git a/tests/integration/package.json b/tests/integration/package.json index 88d67032..28e94aef 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -7,7 +7,7 @@ "@coral-xyz/anchor": "0.31.1" }, "devDependencies": { - "@magicblock-labs/ephemeral-rollups-sdk": "file:../../../ts", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.10", "@metaplex-foundation/beet": "^0.7.1", "@metaplex-foundation/beet-solana": "^0.4.0", "@types/bn.js": "^5.1.0", @@ -20,4 +20,4 @@ "typescript": "^4.3.5" }, "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" -} +} \ No newline at end of file diff --git a/tests/integration/programs/test-delegation/Cargo.toml b/tests/integration/programs/test-delegation/Cargo.toml index 85123852..6405e05e 100644 --- a/tests/integration/programs/test-delegation/Cargo.toml +++ b/tests/integration/programs/test-delegation/Cargo.toml @@ -18,4 +18,4 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = "0.31.1" -ephemeral-rollups-sdk = { version = "0.2.10", path = "../../../../../rust/sdk", features = ["anchor"] } +ephemeral-rollups-sdk = { version = "0.2.10", features = ["anchor"] } diff --git a/tests/integration/programs/test-delegation/src/lib.rs b/tests/integration/programs/test-delegation/src/lib.rs index f9dfad37..979388c2 100644 --- a/tests/integration/programs/test-delegation/src/lib.rs +++ b/tests/integration/programs/test-delegation/src/lib.rs @@ -37,7 +37,10 @@ pub mod test_delegation { ctx.accounts.delegate_pda( &ctx.accounts.payer, &[TEST_PDA_SEED], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; Ok(()) } @@ -47,12 +50,18 @@ pub mod test_delegation { ctx.accounts.delegate_pda( &ctx.accounts.payer, &[TEST_PDA_SEED], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; ctx.accounts.delegate_pda_other( &ctx.accounts.payer, &[TEST_PDA_SEED_OTHER], - DelegateConfig::default(), + DelegateConfig { + commit_frequency_ms: u32::MAX, + validator: Some(ctx.accounts.validator.key()), + }, )?; msg!( "Delegated {:?}, owner {:?}", @@ -152,6 +161,8 @@ pub fn transfer_from_undelegated( #[derive(Accounts)] pub struct DelegateInput<'info> { pub payer: Signer<'info>, + /// CHECK: The validator account + pub validator: AccountInfo<'info>, /// CHECK: The pda to delegate #[account(mut, del, seeds = [TEST_PDA_SEED], bump)] pub pda: AccountInfo<'info>, @@ -161,6 +172,8 @@ pub struct DelegateInput<'info> { #[derive(Accounts)] pub struct DelegateInputTwo<'info> { pub payer: Signer<'info>, + /// CHECK: The validator account + pub validator: AccountInfo<'info>, /// CHECK: The pda to delegate #[account(mut, del, seeds = [TEST_PDA_SEED], bump)] pub pda: AccountInfo<'info>, diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index d8a378ab..2031b2a0 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -9,9 +9,12 @@ import { DELEGATION_PROGRAM_ID, } from "@magicblock-labs/ephemeral-rollups-sdk"; import { ON_CURVE_ACCOUNT } from "./fixtures/consts"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; const SEED_TEST_PDA = "test-pda"; -const BPF_LOADER = new web3.PublicKey("BPFLoaderUpgradeab1e11111111111111111111111") +const BPF_LOADER = new web3.PublicKey( + "BPFLoaderUpgradeab1e11111111111111111111111" +); describe("TestDelegation", () => { // Configure the client to use the local cluster. @@ -49,12 +52,6 @@ describe("TestDelegation", () => { console.log("Claim validator fee vault tx:", txId); }); - it("Claim protocol fees", async () => { - const ix = createClaimProtocolFeesVaultInstruction(admin); - const txId = await processInstruction(ix); - console.log("Claim protocol fee vault tx:", txId); - }); - it("Initializes the counter", async () => { // Check if the counter is initialized const counterAccountInfo = await provider.connection.getAccountInfo(pda); @@ -105,6 +102,7 @@ describe("TestDelegation", () => { .delegateTwo() .accounts({ payer: provider.wallet.publicKey, + validator, }) .rpc({ skipPreflight: true }); console.log("Your transaction signature", tx); @@ -156,7 +154,7 @@ describe("TestDelegation", () => { slot: new anchor.BN(1), lamports: new anchor.BN(1000000000), allow_undelegation: false, - data: new_data, + data: Uint8Array.from(new_data), }; const ix = createCommitAccountInstruction( validator, @@ -183,7 +181,7 @@ describe("TestDelegation", () => { slot: new anchor.BN(2), lamports: new anchor.BN(1000000000), allow_undelegation: true, - data: new_data, + data: Uint8Array.from(new_data), }; const ix = createCommitAccountInstruction( validator, @@ -223,6 +221,26 @@ describe("TestDelegation", () => { console.log("Whitelist a validator for a program:", txId); }); + it("Set fees receiver", async () => { + const ix = createSetFeesReceiverInstruction( + admin, + admin, + testDelegation.programId + ); + const txId = await processInstruction(ix); + console.log("Set fees receiver tx:", txId); + }); + + it("Claim protocol fees", async () => { + const ix = createClaimProtocolFeesVaultInstruction( + admin, + admin, + testDelegation.programId + ); + const txId = await processInstruction(ix); + console.log("Claim protocol fees tx:", txId); + }); + async function processInstruction(ix: web3.TransactionInstruction) { const tx = new web3.Transaction().add(ix); tx.recentBlockhash = ( @@ -410,8 +428,8 @@ describe("TestDelegation", () => { ) { const validatorFeesVault = validatorFeesVaultPdaFromValidator(validator); const delegationProgramData = web3.PublicKey.findProgramAddressSync( - [DELEGATION_PROGRAM_ID.toBuffer()], - BPF_LOADER + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER )[0]; const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, @@ -435,9 +453,7 @@ describe("TestDelegation", () => { } /// Instruction to claim fees from the validator vault - function createClaimValidatorFeesVaultInstruction( - validator: web3.PublicKey - ) { + function createClaimValidatorFeesVaultInstruction(validator: web3.PublicKey) { const feesVault = feesVaultPda(); const validatorFeesVault = validatorFeesVaultPdaFromValidator(validator); const keys = [ @@ -454,19 +470,54 @@ describe("TestDelegation", () => { return ix; } + /// Instruction to set fees receiver + function createSetFeesReceiverInstruction( + admin: web3.PublicKey, + feesReceiver: web3.PublicKey, + program: web3.PublicKey + ) { + const programConfig = programConfigPdaFromProgramId(program); + const delegationProgramData = web3.PublicKey.findProgramAddressSync( + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER + )[0]; + const keys = [ + { pubkey: admin, isSigner: true, isWritable: true }, + { pubkey: programConfig, isSigner: false, isWritable: true }, + { pubkey: program, isSigner: false, isWritable: false }, + { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: delegationProgramData, isSigner: false, isWritable: false }, + ]; + const data = Buffer.from( + [16, 0, 0, 0, 0, 0, 0, 0].concat([...feesReceiver.toBytes()]) + ); + const ix = new web3.TransactionInstruction({ + programId: new web3.PublicKey(DELEGATION_PROGRAM_ID), + keys, + data, + }); + return ix; + } + /// Instruction to claim fees from the protocol vault function createClaimProtocolFeesVaultInstruction( - admin: web3.PublicKey + admin: web3.PublicKey, + feesReceiver: web3.PublicKey, + program: web3.PublicKey ) { const feesVault = feesVaultPda(); + const programConfig = programConfigPdaFromProgramId(program); const delegationProgramData = web3.PublicKey.findProgramAddressSync( - [DELEGATION_PROGRAM_ID.toBuffer()], - BPF_LOADER + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER )[0]; const keys = [ { pubkey: admin, isSigner: true, isWritable: true }, { pubkey: feesVault, isSigner: false, isWritable: true }, - { pubkey: delegationProgramData, isSigner: false, isWritable: true }, + { pubkey: programConfig, isSigner: false, isWritable: true }, + { pubkey: feesReceiver, isSigner: false, isWritable: true }, + { pubkey: program, isSigner: false, isWritable: false }, + { pubkey: delegationProgramData, isSigner: false, isWritable: false }, ]; const data = Buffer.from([12, 0, 0, 0, 0, 0, 0, 0, 0]); const ix = new web3.TransactionInstruction({ @@ -485,10 +536,11 @@ describe("TestDelegation", () => { ) { const programData = web3.PublicKey.findProgramAddressSync( [program.toBuffer()], - BPF_LOADER)[0]; + BPF_LOADER + )[0]; const delegationProgramData = web3.PublicKey.findProgramAddressSync( - [DELEGATION_PROGRAM_ID.toBuffer()], - BPF_LOADER + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER )[0]; const programConfig = programConfigPdaFromProgramId(program); const keys = [ diff --git a/tests/integration/yarn.lock b/tests/integration/yarn.lock index 40ba2952..e8db6d4e 100644 --- a/tests/integration/yarn.lock +++ b/tests/integration/yarn.lock @@ -40,24 +40,16 @@ buffer-layout "^1.2.0" "@magicblock-labs/ephemeral-rollups-sdk@^0.2.10": - version "0.2.10" - resolved "https://registry.yarnpkg.com/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.2.10.tgz#b4552764c7a8caddbe47e892c2de79c1fad29c87" - integrity sha512-JjXBG8tnQrt53HE+MMkNIMr+wCeseVFUUtuXPRspu8pmAAlnDpCmLd76mTV4Jd46emRj0Tt4DKJUjQgXBrtxEQ== - dependencies: - "@metaplex-foundation/beet" "^0.7.2" - "@phala/dcap-qvl-web" "^0.2.7" - "@solana/web3.js" "^1.98.0" - bs58 "^6.0.0" - rpc-websockets "^9.0.4" - -"@magicblock-labs/ephemeral-rollups-sdk@file:../../../ts": - version "0.2.10" + version "0.2.11" + resolved "https://registry.npmjs.org/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.2.11.tgz" + integrity sha512-I9pqYBsWhkUpndt9b4P6jqpVhAKaMdyyiBuxEX+Y2VdWeXuRpF0qXngyn26i1oKCVvbPNQYLu033LpNeSB4BKg== dependencies: "@metaplex-foundation/beet" "^0.7.2" "@phala/dcap-qvl-web" "^0.2.7" "@solana/web3.js" "^1.98.0" bs58 "^6.0.0" rpc-websockets "^9.0.4" + typescript "^5.3.0" "@metaplex-foundation/beet-solana@^0.4.0": version "0.4.1" @@ -69,7 +61,7 @@ bs58 "^5.0.0" debug "^4.3.4" -"@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.7.1", "@metaplex-foundation/beet@^0.7.2": +"@metaplex-foundation/beet@^0.7.1", "@metaplex-foundation/beet@^0.7.2", "@metaplex-foundation/beet@>=0.1.0": version "0.7.2" resolved "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.2.tgz" integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== @@ -86,19 +78,19 @@ dependencies: "@noble/hashes" "1.6.0" -"@noble/hashes@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz" - integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== - "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": version "1.4.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + "@phala/dcap-qvl-web@^0.2.7": version "0.2.7" - resolved "https://registry.yarnpkg.com/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz#d7a03b059a201355262ca9c1bb6c77a1c22472dd" + resolved "https://registry.npmjs.org/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz" integrity sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ== "@solana/buffer-layout@^4.0.1": @@ -213,9 +205,9 @@ "@types/node" "*" "@types/ws@^8.2.2": - version "8.18.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" - integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + version "8.5.13" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== dependencies: "@types/node" "*" @@ -224,14 +216,6 @@ resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - agentkeepalive@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" @@ -321,7 +305,7 @@ base-x@^4.0.0: base-x@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + resolved "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz" integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== base64-js@^1.3.1: @@ -384,7 +368,7 @@ bs58@^5.0.0: bs58@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + resolved "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz" integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== dependencies: base-x "^5.0.0" @@ -399,7 +383,7 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2: resolved "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz" integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== -buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: +buffer@^6.0.3, buffer@~6.0.3, buffer@6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -521,13 +505,6 @@ cross-fetch@^3.1.5: dependencies: node-fetch "^2.6.12" -debug@4.3.3: - version "4.3.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -535,6 +512,13 @@ debug@^4.3.3, debug@^4.3.4: dependencies: ms "2.1.2" +debug@4.3.3: + version "4.3.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" @@ -570,16 +554,16 @@ delay@^5.0.0: resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== -diff@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - diff@^3.1.0: version "3.5.0" resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -794,7 +778,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@^2.0.3, inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -891,13 +875,13 @@ jayson@^4.1.1: "@types/connect" "^3.4.33" "@types/node" "^12.12.54" "@types/ws" "^7.4.4" - JSONStream "^1.3.5" commander "^2.20.3" delay "^5.0.0" es6-promisify "^5.0.0" eyes "^0.1.8" isomorphic-ws "^4.0.1" json-stringify-safe "^5.0.1" + JSONStream "^1.3.5" uuid "^8.3.2" ws "^7.5.10" @@ -925,6 +909,14 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" @@ -952,13 +944,6 @@ make-error@^1.1.1: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== - dependencies: - brace-expansion "^1.1.7" - minimatch@^3.0.4: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -966,6 +951,13 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -978,7 +970,7 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" -mocha@^9.0.3: +"mocha@^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X", mocha@^9.0.3: version "9.2.2" resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== @@ -1008,7 +1000,7 @@ mocha@^9.0.3: yargs-parser "20.2.4" yargs-unparser "2.0.0" -ms@2.1.2, ms@^2.0.0: +ms@^2.0.0, ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -1138,7 +1130,7 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -rpc-websockets@^9.0.2: +rpc-websockets@^9.0.2, rpc-websockets@^9.0.4: version "9.0.4" resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.4.tgz" integrity sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ== @@ -1154,22 +1146,6 @@ rpc-websockets@^9.0.2: bufferutil "^4.0.1" utf-8-validate "^5.0.2" -rpc-websockets@^9.0.4: - version "9.1.3" - resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.1.3.tgz#6805dfc01232389dab043861ab9fbfe9d1d75572" - integrity sha512-I+kNjW0udB4Fetr3vvtRuYZJS0PcSPyyvBcH5sDdoV8DFs5E4W2pTr7aiMlKfPxANTClP9RlqCPolj9dd5MsEA== - dependencies: - "@swc/helpers" "^0.5.11" - "@types/uuid" "^8.3.4" - "@types/ws" "^8.2.2" - buffer "^6.0.3" - eventemitter3 "^5.0.1" - uuid "^8.3.2" - ws "^8.5.0" - optionalDependencies: - bufferutil "^4.0.1" - utf-8-validate "^5.0.2" - safe-buffer@^5.0.1, safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" @@ -1243,13 +1219,6 @@ superstruct@^2.0.2: resolved "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" @@ -1257,6 +1226,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + text-encoding-utf-8@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" @@ -1327,17 +1303,22 @@ type-detect@^4.0.0, type-detect@^4.0.8: resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -typescript@^4.3.5: +typescript@^4.3.5, typescript@>=5: version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.0: + version "5.9.2" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -utf-8-validate@^5.0.2: +utf-8-validate@^5.0.2, utf-8-validate@>=5.0.2: version "5.0.10" resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== @@ -1410,7 +1391,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@^7.5.10: +ws@*, ws@^7.5.10: version "7.5.10" resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -1425,7 +1406,7 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yargs-parser@20.2.4, yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== diff --git a/tests/test_commit_state_with_program_config.rs b/tests/test_commit_state_with_program_config.rs index 889e8a0a..2d9716a6 100644 --- a/tests/test_commit_state_with_program_config.rs +++ b/tests/test_commit_state_with_program_config.rs @@ -166,11 +166,14 @@ async fn setup_program_test_env(valid_config: bool) -> (BanksClient, Keypair, Ke ); // Setup the program config - let program_config_data = create_program_config_data(if valid_config { - validator_keypair.pubkey() - } else { - Keypair::new().pubkey() - }); + let program_config_data = create_program_config_data( + if valid_config { + validator_keypair.pubkey() + } else { + Keypair::new().pubkey() + }, + validator_keypair.pubkey(), + ); program_test.add_account( program_config_from_program_id(&DELEGATED_PDA_OWNER_ID), Account { diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 8218ccb0..7f6af377 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -1,8 +1,9 @@ -use crate::fixtures::TEST_AUTHORITY; -use dlp::pda::fees_vault_pda; +use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; +use dlp::pda::{fees_vault_pda, program_config_from_program_id}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::pubkey::Pubkey; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -19,7 +20,7 @@ async fn test_protocol_claim_fees() { let fees_vault_pda = fees_vault_pda(); // Submit the claim fees tx - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), @@ -74,6 +75,18 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { }, ); + // Setup the fees program config account + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_program_config_data(Pubkey::new_unique(), admin_keypair.pubkey()), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + let (banks, payer, blockhash) = program_test.start().await; (banks, payer, admin_keypair, blockhash) } diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs new file mode 100644 index 00000000..09e5ccfb --- /dev/null +++ b/tests/test_set_fees_receiver.rs @@ -0,0 +1,208 @@ +use std::collections::BTreeSet; + +use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; +use borsh::{BorshDeserialize, BorshSerialize}; +use dlp::pda::{fees_vault_pda, program_config_from_program_id}; +use dlp::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; +use dlp::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +mod fixtures; + +#[derive(BorshSerialize, BorshDeserialize)] +struct OldProgramConfig { + approved_validators: BTreeSet, +} + +impl AccountWithDiscriminator for OldProgramConfig { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::ProgramConfig + } +} + +impl_to_bytes_with_discriminator_borsh!(OldProgramConfig); +impl_try_from_bytes_with_discriminator_borsh!(OldProgramConfig); + +#[tokio::test] +async fn test_set_fees_receiver() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env(false).await; + + let fees_vault_pda = fees_vault_pda(); + + let fees_receiver = Pubkey::new_unique(); + + // Set the fees receiver to a new account + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Try claiming to the wrong fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); + + // Claim to the correct fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Assert that fees vault now only have the rent exemption amount + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); + assert!(fees_vault_account.is_some()); + assert_eq!( + fees_vault_account.unwrap().lamports, + Rent::default().minimum_balance(8) + ); + + // Assert that the fees receiver account now has the fees + let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); + assert_eq!( + fees_receiver_account.unwrap().lamports, + LAMPORTS_PER_SOL - Rent::default().minimum_balance(8) + ); +} + +#[tokio::test] +async fn test_set_fees_receiver_migration() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env(true).await; + + let fees_vault_pda = fees_vault_pda(); + + let fees_receiver = Pubkey::new_unique(); + + // Set the fees receiver to a new account + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Try claiming to the wrong fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); + + // Claim to the correct fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Assert that fees vault now only have the rent exemption amount + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); + assert!(fees_vault_account.is_some()); + assert_eq!( + fees_vault_account.unwrap().lamports, + Rent::default().minimum_balance(8) + ); + + // Assert that the fees receiver account now has the fees + let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); + assert_eq!( + fees_receiver_account.unwrap().lamports, + LAMPORTS_PER_SOL - Rent::default().minimum_balance(8) + ); +} + +async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + program_test.prefer_bpf(true); + + let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + admin_keypair.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the fees vault account + program_test.add_account( + fees_vault_pda(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the fees program config account + let data = if migrate { + let mut program_config = OldProgramConfig { + approved_validators: BTreeSet::new(), + }; + program_config + .approved_validators + .insert(Pubkey::new_unique()); + let mut bytes = vec![]; + program_config + .to_bytes_with_discriminator(&mut bytes) + .unwrap(); + bytes + } else { + create_program_config_data(Pubkey::new_unique(), admin_keypair.pubkey()) + }; + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, admin_keypair, blockhash) +} From fa719199d54c0814a1d15e6124515d8586e8b737 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 15 Sep 2025 18:45:07 +0200 Subject: [PATCH 02/29] feat: remote claim admin --- .../protocol_claim_fees.rs | 8 ++--- src/processor/protocol_claim_fees.rs | 20 +---------- tests/integration/tests/test-delegation.ts | 8 ----- tests/test_protocol_claim_fees.rs | 9 ++--- tests/test_set_fees_receiver.rs | 36 +++++-------------- 5 files changed, 13 insertions(+), 68 deletions(-) diff --git a/src/instruction_builder/protocol_claim_fees.rs b/src/instruction_builder/protocol_claim_fees.rs index e687cc0f..4a5b424e 100644 --- a/src/instruction_builder/protocol_claim_fees.rs +++ b/src/instruction_builder/protocol_claim_fees.rs @@ -1,25 +1,21 @@ use solana_program::instruction::Instruction; -use solana_program::{bpf_loader_upgradeable, instruction::AccountMeta, pubkey::Pubkey}; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; use crate::discriminator::DlpDiscriminator; use crate::pda::{fees_vault_pda, program_config_from_program_id}; /// Claim the accrued fees from the protocol fees vault. /// See [crate::processor::process_protocol_claim_fees] for docs. -pub fn protocol_claim_fees(admin: Pubkey, fees_receiver: Pubkey, program: Pubkey) -> Instruction { +pub fn protocol_claim_fees(fees_receiver: Pubkey, program: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); let program_config_pda = program_config_from_program_id(&program); - let delegation_program_data = - Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { program_id: crate::id(), accounts: vec![ - AccountMeta::new(admin, true), AccountMeta::new(fees_vault_pda, false), AccountMeta::new(program_config_pda, false), AccountMeta::new(fees_receiver, false), AccountMeta::new_readonly(program, false), - AccountMeta::new_readonly(delegation_program_data, false), ], data: DlpDiscriminator::ProtocolClaimFees.to_vec(), } diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 41cdbd9b..28b6b3b0 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -1,10 +1,7 @@ -use crate::error::DlpError::Unauthorized; use crate::processor::utils::loaders::{ load_account, load_initialized_protocol_fees_vault, load_program_config, - load_program_upgrade_authority, load_signer, }; use crate::state::ProgramConfig; -use solana_program::msg; use solana_program::program_error::ProgramError; use solana_program::rent::Rent; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; @@ -33,29 +30,14 @@ pub fn process_protocol_claim_fees( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [admin, fees_vault, program_config_account, fees_receiver, program, delegation_program_data] = - accounts - else { + let [fees_vault, program_config_account, fees_receiver, program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; // Check if the admin is signer - load_signer(admin, "admin")?; load_initialized_protocol_fees_vault(fees_vault, true)?; load_program_config(program_config_account, *program.key, true)?; - // Check if the admin is the correct one - let admin_pubkey = - load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; - if !admin.key.eq(&admin_pubkey) { - msg!( - "Expected admin pubkey: {} but got {}", - admin_pubkey, - admin.key - ); - return Err(Unauthorized.into()); - } - let program_config_data = program_config_account.try_borrow_data()?; let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)?; diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index 2031b2a0..e23d661d 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -233,7 +233,6 @@ describe("TestDelegation", () => { it("Claim protocol fees", async () => { const ix = createClaimProtocolFeesVaultInstruction( - admin, admin, testDelegation.programId ); @@ -501,23 +500,16 @@ describe("TestDelegation", () => { /// Instruction to claim fees from the protocol vault function createClaimProtocolFeesVaultInstruction( - admin: web3.PublicKey, feesReceiver: web3.PublicKey, program: web3.PublicKey ) { const feesVault = feesVaultPda(); const programConfig = programConfigPdaFromProgramId(program); - const delegationProgramData = web3.PublicKey.findProgramAddressSync( - [DELEGATION_PROGRAM_ID.toBuffer()], - BPF_LOADER - )[0]; const keys = [ - { pubkey: admin, isSigner: true, isWritable: true }, { pubkey: feesVault, isSigner: false, isWritable: true }, { pubkey: programConfig, isSigner: false, isWritable: true }, { pubkey: feesReceiver, isSigner: false, isWritable: true }, { pubkey: program, isSigner: false, isWritable: false }, - { pubkey: delegationProgramData, isSigner: false, isWritable: false }, ]; const data = Buffer.from([12, 0, 0, 0, 0, 0, 0, 0, 0]); const ix = new web3.TransactionInstruction({ diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 7f6af377..1c2c0b91 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -20,13 +20,8 @@ async fn test_protocol_claim_fees() { let fees_vault_pda = fees_vault_pda(); // Submit the claim fees tx - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 09e5ccfb..d0f1e3b7 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -52,24 +52,14 @@ async fn test_set_fees_receiver() { assert!(res.is_ok()); // Try claiming to the wrong fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_err()); // Claim to the correct fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), fees_receiver, dlp::ID); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); @@ -110,24 +100,14 @@ async fn test_set_fees_receiver_migration() { assert!(res.is_ok()); // Try claiming to the wrong fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), admin.pubkey(), dlp::ID); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_err()); // Claim to the correct fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), fees_receiver, dlp::ID); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); From 1ff8e9807d39c057fa602bbcee0d6689a3a1587c Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 15 Sep 2025 18:55:35 +0200 Subject: [PATCH 03/29] docs: fix --- src/processor/protocol_claim_fees.rs | 11 +++++------ src/processor/set_fees_receiver.rs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 28b6b3b0..48ccd946 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -10,18 +10,17 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// /// Accounts: /// -/// 1. `[signer]` admin account that can claim the fees -/// 2. `[writable]` protocol fees vault PDA -/// 3. `[]` program config PDA -/// 4. `[writable]` fees receiver PDA -/// 5. `[]` delegation program +/// 1. `[writable]` protocol fees vault PDA +/// 2. `[]` program config PDA +/// 3. `[writable]` fees receiver PDA +/// 4. `[]` delegation program /// /// Requirements: /// /// - protocol fees vault is initialized /// - protocol fees vault has enough lamports to claim fees and still be /// rent exempt -/// - admin is the protocol fees vault admin +/// - fees receiver is the correct one /// /// 1. Transfer lamports from protocol fees_vault PDA to the admin authority pub fn process_protocol_claim_fees( diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs index 1c773420..7a0c9d12 100644 --- a/src/processor/set_fees_receiver.rs +++ b/src/processor/set_fees_receiver.rs @@ -10,7 +10,7 @@ use solana_program::msg; use solana_program::program_error::ProgramError; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; -/// Process request to claim fees from the protocol fees vault +/// Process request to set the fees receiver /// /// Accounts: /// From ab071308699d70b8cc4845dde1c23a40a3cc732e Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 28 Oct 2025 17:58:52 +0100 Subject: [PATCH 04/29] Merge branch 'main' into feat/fee-receiver --- .github/workflows/run-tests.yml | 4 - Cargo.lock | 95 +- Cargo.toml | 7 +- src/args/call_handler.rs | 10 +- src/consts.rs | 4 - src/cu.rs | 41 + src/discriminator.rs | 26 - src/entrypoint.rs | 44 + src/error.rs | 8 + src/lib.rs | 118 +- src/pda.rs | 43 +- src/processor/call_handler.rs | 37 +- src/processor/commit_state_from_buffer.rs | 76 - src/processor/delegate.rs | 178 -- src/processor/{ => fast}/commit_state.rs | 191 +-- .../fast/commit_state_from_buffer.rs | 46 + src/processor/fast/delegate.rs | 227 +++ src/processor/{ => fast}/finalize.rs | 82 +- src/processor/fast/mod.rs | 18 + src/processor/{ => fast}/undelegate.rs | 244 +-- src/processor/fast/utils/mod.rs | 2 + src/processor/fast/utils/pda.rs | 137 ++ src/processor/fast/utils/requires.rs | 337 ++++ src/processor/mod.rs | 12 +- src/processor/utils/curve.rs | 28 +- src/processor/utils/loaders.rs | 128 +- src/processor/utils/pda.rs | 52 - src/state/delegation_metadata.rs | 10 + src/state/utils/discriminator.rs | 4 +- tests/buffers/test_delegation.so | Bin 291088 -> 292392 bytes tests/integration/Cargo.lock | 16 +- tests/integration/package-lock.json | 5 +- tests/integration/package.json | 2 +- .../programs/test-delegation/Cargo.toml | 2 +- .../programs/test-delegation/src/lib.rs | 120 +- tests/integration/tests/test-delegation.ts | 97 +- tests/integration/yarn.lock | 1445 ----------------- tests/test_call_handler.rs | 35 +- tests/test_close_validator_fees_vault.rs | 4 +- tests/test_commit_on_curve.rs | 4 +- tests/test_commit_state.rs | 4 +- tests/test_commit_state_from_buffer.rs | 4 +- .../test_commit_state_with_program_config.rs | 4 +- tests/test_delegate.rs | 5 +- tests/test_delegate_on_curve.rs | 4 +- tests/test_finalize.rs | 4 +- tests/test_init_fees_vault.rs | 4 +- tests/test_init_validator_fees_vault.rs | 4 +- tests/test_lamports_settlement.rs | 4 +- tests/test_protocol_claim_fees.rs | 6 +- tests/test_top_up.rs | 4 +- tests/test_undelegate.rs | 4 +- tests/test_undelegate_on_curve.rs | 4 +- tests/test_undelegate_without_commit.rs | 4 +- tests/test_validator_claim_fees.rs | 4 +- tests/test_whitelist_validator_for_program.rs | 4 +- 56 files changed, 1614 insertions(+), 2392 deletions(-) create mode 100644 src/cu.rs create mode 100644 src/entrypoint.rs delete mode 100644 src/processor/commit_state_from_buffer.rs delete mode 100644 src/processor/delegate.rs rename src/processor/{ => fast}/commit_state.rs (61%) create mode 100644 src/processor/fast/commit_state_from_buffer.rs create mode 100644 src/processor/fast/delegate.rs rename src/processor/{ => fast}/finalize.rs (69%) create mode 100644 src/processor/fast/mod.rs rename src/processor/{ => fast}/undelegate.rs (59%) create mode 100644 src/processor/fast/utils/mod.rs create mode 100644 src/processor/fast/utils/pda.rs create mode 100644 src/processor/fast/utils/requires.rs delete mode 100644 tests/integration/yarn.lock diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b713e9b6..0c2b37b8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -87,7 +87,6 @@ jobs: - name: clean up before integration tests run: | - rm -rf sdk/ts/node_modules cp target/deploy/dlp.so /tmp/dlp.so rm -rf target mkdir -p target/deploy && cp /tmp/dlp.so target/deploy/dlp.so @@ -98,10 +97,7 @@ jobs: - name: run integration tests run: | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" - npm install -g @magicblock-labs/bolt-cli - cd sdk/ts && npm install --legacy-peer-deps && npm run build && npm run lint:fix && cd ../.. cd tests/integration - set -x cargo install --locked --version ${{ env.anchor_version }} anchor-cli npm install anchor test diff --git a/Cargo.lock b/Cargo.lock index 7c820d6a..310d545b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,11 +501,11 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" dependencies = [ - "borsh-derive 1.5.5", + "borsh-derive 1.5.7", "cfg_aliases", ] @@ -524,9 +524,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -1322,9 +1322,9 @@ dependencies = [ [[package]] name = "five8_const" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b4f62f0f8ca357f93ae90c8c2dd1041a1f665fde2f889ea9b1787903829015" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" dependencies = [ "five8_core", ] @@ -2219,15 +2219,19 @@ dependencies = [ [[package]] name = "magicblock-delegation-program" -version = "1.1.0" +version = "1.1.2" dependencies = [ "base64 0.22.1", "bincode", - "borsh 1.5.5", + "borsh 1.5.7", "bytemuck", "magicblock-delegation-program", "num_enum", "paste", + "pinocchio", + "pinocchio-log", + "pinocchio-pubkey", + "pinocchio-system", "rand 0.8.5", "solana-curve25519", "solana-program", @@ -2739,6 +2743,53 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinocchio" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b971851087bc3699b001954ad02389d50c41405ece3548cbcafc88b3e20017a" + +[[package]] +name = "pinocchio-log" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f266eb7ddac75ef32c42025d5cc571da4f8c9e6d5c3d70820c204d5fee72e57a" +dependencies = [ + "pinocchio-log-macro", +] + +[[package]] +name = "pinocchio-log-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fb52edb3c5736b044cc462b0957b9767d0f574d138f4e2761438c498a4b467" +dependencies = [ + "quote", + "regex", + "syn 1.0.109", +] + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const", + "pinocchio", + "sha2-const-stable", +] + +[[package]] +name = "pinocchio-system" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141ed5eafb4ab04568bb0e224e3dc9a9de13c933de4c004e0d1a553498be3a7c" +dependencies = [ + "pinocchio", + "pinocchio-pubkey", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -3536,6 +3587,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "sha3" version = "0.10.8" @@ -3770,7 +3827,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b8593f50e34f44ed168dccbac0a9b8e43e138ac75102dac6635fbdfff88d1e" dependencies = [ - "borsh 1.5.5", + "borsh 1.5.7", "futures", "solana-banks-interface", "solana-program", @@ -3871,7 +3928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" dependencies = [ "borsh 0.10.3", - "borsh 1.5.5", + "borsh 1.5.7", ] [[package]] @@ -4126,7 +4183,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a5df17b195d312b66dccdde9beec6709766d8230cb4718c4c08854f780d0309" dependencies = [ - "borsh 1.5.5", + "borsh 1.5.7", "serde", "serde_derive", "solana-instruction", @@ -4472,7 +4529,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf7bcb14392900fe02e4e34e90234fbf0c673d4e327888410ba99fa2ba0f4e99" dependencies = [ - "borsh 1.5.5", + "borsh 1.5.7", "bs58", "bytemuck", "bytemuck_derive", @@ -4511,7 +4568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce496a475e5062ba5de97215ab39d9c358f9c9df4bb7f3a45a1f1a8bd9065ed" dependencies = [ "bincode", - "borsh 1.5.5", + "borsh 1.5.7", "getrandom 0.2.15", "js-sys", "num-traits", @@ -4932,7 +4989,7 @@ dependencies = [ "bincode", "blake3", "borsh 0.10.3", - "borsh 1.5.5", + "borsh 1.5.7", "bs58", "bytemuck", "console_error_panic_hook", @@ -5021,7 +5078,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8ae2c1a8d0d4ae865882d5770a7ebca92bab9c685e43f0461682c6c05a35bfa" dependencies = [ - "borsh 1.5.5", + "borsh 1.5.7", "num-traits", "serde", "serde_derive", @@ -5141,7 +5198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40db1ff5a0f8aea2c158d78ab5f2cf897848964251d1df42fef78efd3c85b863" dependencies = [ "borsh 0.10.3", - "borsh 1.5.5", + "borsh 1.5.7", "bs58", "bytemuck", "bytemuck_derive", @@ -5632,7 +5689,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ - "borsh 1.5.5", + "borsh 1.5.7", "libsecp256k1", "solana-define-syscall", "thiserror 2.0.11", @@ -5826,7 +5883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" dependencies = [ "borsh 0.10.3", - "borsh 1.5.5", + "borsh 1.5.7", "num-traits", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 1a849512..0aaee9dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "magicblock-delegation-program" description = "Delegation program for the Ephemeral Rollups" -version = "1.1.0" +version = "1.1.2" authors = ["Magicblock Labs "] edition = "2021" license = "MIT" @@ -29,6 +29,7 @@ name = "dlp" no-entrypoint = [] default = ["solana-security-txt"] unit_test_config = [] +log-cost = [] [dependencies] borsh = { version = "1.5.3", features = [ "derive" ] } @@ -40,6 +41,10 @@ thiserror = "^1.0.57" solana-security-txt = { version = "1.1.1", optional = true } solana-curve25519 = "2.2" bincode = "1.3.3" +pinocchio-log = "0.5.0" +pinocchio = "0.9.2" +pinocchio-pubkey = "0.3.0" +pinocchio-system = "0.3.0" [dev-dependencies] base64 = "0.22.1" diff --git a/src/args/call_handler.rs b/src/args/call_handler.rs index 2c9c03ed..98d9cf57 100644 --- a/src/args/call_handler.rs +++ b/src/args/call_handler.rs @@ -1,15 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; -#[derive(BorshSerialize, BorshDeserialize, Clone, Copy)] -pub enum Context { - Commit, - Undelegate, - Standalone, -} - #[derive(BorshSerialize, BorshDeserialize)] pub struct CallHandlerArgs { pub escrow_index: u8, + /// This is raw instruction data, it could include discriminator + args + /// or can be in any other custom format pub data: Vec, - pub context: Context, } diff --git a/src/consts.rs b/src/consts.rs index 8c95732c..72c1abaa 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -10,10 +10,6 @@ pub const PROTOCOL_FEES_PERCENTAGE: u8 = 10; /// The discriminator for the external undelegate instruction. pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = [196, 28, 41, 206, 48, 37, 51, 167]; -/// The discriminator for the external hook after finalization is complete -/// For anchor: corresponds to function/instruction name delegation_program_call_handler -pub const EXTERNAL_CALL_HANDLER_DISCRIMINATOR: [u8; 8] = [157, 197, 228, 30, 0, 80, 121, 135]; - /// The program ID of the delegation program. pub const DELEGATION_PROGRAM_ID: Pubkey = crate::id(); diff --git a/src/cu.rs b/src/cu.rs new file mode 100644 index 00000000..264c6e87 --- /dev/null +++ b/src/cu.rs @@ -0,0 +1,41 @@ +use pinocchio::syscalls::sol_remaining_compute_units; +use pinocchio_log::log; + +pub struct BenchmarkComputeUnit { + name: &'static str, + remaining_at_start: u64, +} + +impl BenchmarkComputeUnit { + pub fn start(name: &'static str) -> BenchmarkComputeUnit { + log!("BENCHMARK BEGIN: [{}]", name); + Self { + name, + remaining_at_start: Self::remaining_cu(), + } + } + + fn remaining_cu() -> u64 { + unsafe { sol_remaining_compute_units() } + } +} + +impl Drop for BenchmarkComputeUnit { + fn drop(&mut self) { + let consumed = self.remaining_at_start - Self::remaining_cu(); + log!( + "BENCHMARK END: [{}] consumed {} of {} compute units.", + self.name, + consumed, + self.remaining_at_start + ) + } +} + +#[macro_export] +macro_rules! compute { + ($msg:expr=> $($tt:tt)*) => { + let _log = $crate::cu::BenchmarkComputeUnit::start($msg); + $($tt)* + }; +} diff --git a/src/discriminator.rs b/src/discriminator.rs index eef4941c..2b5d2e26 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -1,5 +1,4 @@ use num_enum::TryFromPrimitive; -use solana_program::program_error::ProgramError; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] @@ -45,28 +44,3 @@ impl DlpDiscriminator { num.to_le_bytes().to_vec() } } - -impl TryFrom<[u8; 8]> for DlpDiscriminator { - type Error = ProgramError; - fn try_from(bytes: [u8; 8]) -> Result { - match bytes[0] { - 0x0 => Ok(DlpDiscriminator::Delegate), - 0x1 => Ok(DlpDiscriminator::CommitState), - 0x2 => Ok(DlpDiscriminator::Finalize), - 0x3 => Ok(DlpDiscriminator::Undelegate), - 0x5 => Ok(DlpDiscriminator::InitProtocolFeesVault), - 0x6 => Ok(DlpDiscriminator::InitValidatorFeesVault), - 0x7 => Ok(DlpDiscriminator::ValidatorClaimFees), - 0x8 => Ok(DlpDiscriminator::WhitelistValidatorForProgram), - 0x9 => Ok(DlpDiscriminator::TopUpEphemeralBalance), - 0xa => Ok(DlpDiscriminator::DelegateEphemeralBalance), - 0xb => Ok(DlpDiscriminator::CloseEphemeralBalance), - 0xc => Ok(DlpDiscriminator::ProtocolClaimFees), - 0xd => Ok(DlpDiscriminator::CommitStateFromBuffer), - 0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault), - 0xf => Ok(DlpDiscriminator::CallHandler), - 0x10 => Ok(DlpDiscriminator::SetFeesReceiver), - _ => Err(ProgramError::InvalidInstructionData), - } - } -} diff --git a/src/entrypoint.rs b/src/entrypoint.rs new file mode 100644 index 00000000..180f10c4 --- /dev/null +++ b/src/entrypoint.rs @@ -0,0 +1,44 @@ +use crate::{fast_process_instruction, slow_process_instruction}; + +use solana_program::entrypoint; + +entrypoint::custom_heap_default!(); +entrypoint::custom_panic_default!(); + +/// # Safety +/// +/// It's pretty close to the code generated by entrypoint!() macro, with one minor tweak to +/// support fallback branch. +#[no_mangle] +pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 { + const UNINIT: core::mem::MaybeUninit = + core::mem::MaybeUninit::::uninit(); + let mut accounts = [UNINIT; { pinocchio::MAX_TX_ACCOUNTS }]; + + let (program_id, count, data) = + pinocchio::entrypoint::deserialize::<{ pinocchio::MAX_TX_ACCOUNTS }>(input, &mut accounts); + match fast_process_instruction( + program_id, + core::slice::from_raw_parts(accounts.as_ptr() as _, count), + data, + ) { + Some(Ok(())) => pinocchio::SUCCESS, + Some(Err(error)) => error.into(), + + // Fallback to the slow path that does not use pinocchio SDK. + None => slow_entrypoint(input), + } +} + +/// # Safety +/// +/// It's pretty close to the code generated by entrypoint!() macro, with one difference: the +/// function name is slow_entrypoint() as opposed to entrypoint() because this is a fallback +/// entrypoint (a slow one). +pub unsafe fn slow_entrypoint(input: *mut u8) -> u64 { + let (program_id, accounts, instruction_data) = unsafe { entrypoint::deserialize(input) }; + match slow_process_instruction(program_id, &accounts, instruction_data) { + Ok(()) => entrypoint::SUCCESS, + Err(error) => error.into(), + } +} diff --git a/src/error.rs b/src/error.rs index 26add356..3ec84cdc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,6 +33,8 @@ pub enum DlpError { NonceOutOfOrder = 12, #[error("Computation overflow detected")] Overflow = 13, + #[error("Too many seeds")] + TooManySeeds = 14, } impl From for ProgramError { @@ -40,3 +42,9 @@ impl From for ProgramError { ProgramError::Custom(e as u32) } } + +impl From for pinocchio::program_error::ProgramError { + fn from(e: DlpError) -> Self { + pinocchio::program_error::ProgramError::Custom(e as u32) + } +} diff --git a/src/lib.rs b/src/lib.rs index 55d386b4..cdcd6824 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ #![allow(unexpected_cfgs)] // silence clippy for target_os solana and other solana program custom features -use crate::processor::process_call_handler; -use solana_program::{ - account_info::AccountInfo, declare_id, entrypoint::ProgramResult, msg, - program_error::ProgramError, pubkey::Pubkey, -}; +use crate::discriminator::DlpDiscriminator; +use pinocchio_log::log; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use solana_program::{declare_id, msg}; pub mod args; pub mod consts; @@ -15,10 +17,16 @@ pub mod pda; mod processor; pub mod state; +#[cfg(feature = "log-cost")] +mod cu; +#[cfg(not(feature = "no-entrypoint"))] +mod entrypoint; + declare_id!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"); -#[cfg(not(feature = "no-entrypoint"))] -solana_program::entrypoint!(process_instruction); +pub mod fast { + pinocchio_pubkey::declare_id!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"); +} #[cfg(all(not(feature = "no-entrypoint"), feature = "solana-security-txt"))] solana_security_txt::security_txt! { @@ -30,72 +38,96 @@ solana_security_txt::security_txt! { source_code: "https://github.com/magicblock-labs/delegation-program" } -pub fn process_instruction( +pub fn fast_process_instruction( + program_id: &pinocchio::pubkey::Pubkey, + accounts: &[pinocchio::account_info::AccountInfo], + data: &[u8], +) -> Option { + if data.len() < 8 { + return Some(Err( + pinocchio::program_error::ProgramError::InvalidInstructionData, + )); + } + + let (discriminator_bytes, data) = data.split_at(8); + + let discriminator = match DlpDiscriminator::try_from(discriminator_bytes[0]) { + Ok(discriminator) => discriminator, + Err(_) => { + log!("Failed to read and parse discriminator"); + return Some(Err( + pinocchio::program_error::ProgramError::InvalidInstructionData, + )); + } + }; + + match discriminator { + DlpDiscriminator::Delegate => Some(processor::fast::process_delegate( + program_id, accounts, data, + )), + DlpDiscriminator::CommitState => Some(processor::fast::process_commit_state( + program_id, accounts, data, + )), + DlpDiscriminator::CommitStateFromBuffer => Some( + processor::fast::process_commit_state_from_buffer(program_id, accounts, data), + ), + DlpDiscriminator::Finalize => Some(processor::fast::process_finalize( + program_id, accounts, data, + )), + DlpDiscriminator::Undelegate => Some(processor::fast::process_undelegate( + program_id, accounts, data, + )), + _ => None, + } +} + +pub fn slow_process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8], ) -> ProgramResult { - if program_id.ne(&id()) { - return Err(ProgramError::IncorrectProgramId); - } - if data.len() < 8 { return Err(ProgramError::InvalidInstructionData); } let (tag, data) = data.split_at(8); - let tag_array: [u8; 8] = tag - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?; + let ix = DlpDiscriminator::try_from(tag[0]).or(Err(ProgramError::InvalidInstructionData))?; - let ix = discriminator::DlpDiscriminator::try_from(tag_array) - .or(Err(ProgramError::InvalidInstructionData))?; msg!("Processing instruction: {:?}", ix); match ix { - discriminator::DlpDiscriminator::Delegate => { - processor::process_delegate(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::CommitState => { - processor::process_commit_state(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::CommitStateFromBuffer => { - processor::process_commit_state_from_buffer(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::Finalize => { - processor::process_finalize(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::Undelegate => { - processor::process_undelegate(program_id, accounts, data)? - } - discriminator::DlpDiscriminator::InitValidatorFeesVault => { + DlpDiscriminator::InitValidatorFeesVault => { processor::process_init_validator_fees_vault(program_id, accounts, data)? } - discriminator::DlpDiscriminator::InitProtocolFeesVault => { + DlpDiscriminator::InitProtocolFeesVault => { processor::process_init_protocol_fees_vault(program_id, accounts, data)? } - discriminator::DlpDiscriminator::ValidatorClaimFees => { + DlpDiscriminator::ValidatorClaimFees => { processor::process_validator_claim_fees(program_id, accounts, data)? } - discriminator::DlpDiscriminator::WhitelistValidatorForProgram => { + DlpDiscriminator::WhitelistValidatorForProgram => { processor::process_whitelist_validator_for_program(program_id, accounts, data)? } - discriminator::DlpDiscriminator::TopUpEphemeralBalance => { + DlpDiscriminator::TopUpEphemeralBalance => { processor::process_top_up_ephemeral_balance(program_id, accounts, data)? } - discriminator::DlpDiscriminator::DelegateEphemeralBalance => { + DlpDiscriminator::DelegateEphemeralBalance => { processor::process_delegate_ephemeral_balance(program_id, accounts, data)? } - discriminator::DlpDiscriminator::CloseEphemeralBalance => { + DlpDiscriminator::CloseEphemeralBalance => { processor::process_close_ephemeral_balance(program_id, accounts, data)? } - discriminator::DlpDiscriminator::ProtocolClaimFees => { + DlpDiscriminator::ProtocolClaimFees => { processor::process_protocol_claim_fees(program_id, accounts, data)? } - discriminator::DlpDiscriminator::CloseValidatorFeesVault => { + DlpDiscriminator::CloseValidatorFeesVault => { processor::process_close_validator_fees_vault(program_id, accounts, data)? } - discriminator::DlpDiscriminator::CallHandler => { - process_call_handler(program_id, accounts, data)? + DlpDiscriminator::CallHandler => { + processor::process_call_handler(program_id, accounts, data)? + } + _ => { + log!("PANIC: Instruction must be processed by fast_process_instruction"); + return Err(ProgramError::InvalidInstructionData); } discriminator::DlpDiscriminator::SetFeesReceiver => { processor::process_set_fees_receiver(program_id, accounts, data)? diff --git a/src/pda.rs b/src/pda.rs index 8bb65ef7..b5b02db7 100644 --- a/src/pda.rs +++ b/src/pda.rs @@ -1,44 +1,62 @@ use solana_program::pubkey::Pubkey; +pub const DELEGATION_RECORD_TAG: &[u8] = b"delegation"; #[macro_export] macro_rules! delegation_record_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"delegation", &$delegated_account.as_ref()] + &[ + $crate::pda::DELEGATION_RECORD_TAG, + &$delegated_account.as_ref(), + ] }; } +pub const DELEGATION_METADATA_TAG: &[u8] = b"delegation-metadata"; #[macro_export] macro_rules! delegation_metadata_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"delegation-metadata", &$delegated_account.as_ref()] + &[ + $crate::pda::DELEGATION_METADATA_TAG, + &$delegated_account.as_ref(), + ] }; } +pub const COMMIT_STATE_TAG: &[u8] = b"state-diff"; #[macro_export] macro_rules! commit_state_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"state-diff", &$delegated_account.as_ref()] + &[$crate::pda::COMMIT_STATE_TAG, &$delegated_account.as_ref()] }; } +pub const COMMIT_RECORD_TAG: &[u8] = b"commit-state-record"; #[macro_export] macro_rules! commit_record_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"commit-state-record", &$delegated_account.as_ref()] + &[$crate::pda::COMMIT_RECORD_TAG, &$delegated_account.as_ref()] }; } +pub const DELEGATE_BUFFER_TAG: &[u8] = b"buffer"; #[macro_export] macro_rules! delegate_buffer_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"buffer", &$delegated_account.as_ref()] + &[ + $crate::pda::DELEGATE_BUFFER_TAG, + &$delegated_account.as_ref(), + ] }; } +pub const UNDELEGATE_BUFFER_TAG: &[u8] = b"undelegate-buffer"; #[macro_export] macro_rules! undelegate_buffer_seeds_from_delegated_account { ($delegated_account: expr) => { - &[b"undelegate-buffer", &$delegated_account.as_ref()] + &[ + $crate::pda::UNDELEGATE_BUFFER_TAG, + &$delegated_account.as_ref(), + ] }; } @@ -49,24 +67,31 @@ macro_rules! fees_vault_seeds { }; } +pub const VALIDATOR_FEES_VAULT_TAG: &[u8] = b"v-fees-vault"; #[macro_export] macro_rules! validator_fees_vault_seeds_from_validator { ($validator: expr) => { - &[b"v-fees-vault", &$validator.as_ref()] + &[$crate::pda::VALIDATOR_FEES_VAULT_TAG, &$validator.as_ref()] }; } +pub const PROGRAM_CONFIG_TAG: &[u8] = b"p-conf"; #[macro_export] macro_rules! program_config_seeds_from_program_id { ($program_id: expr) => { - &[b"p-conf", &$program_id.as_ref()] + &[$crate::pda::PROGRAM_CONFIG_TAG, &$program_id.as_ref()] }; } +pub const EPHEMERAL_BALANCE_TAG: &[u8] = b"balance"; #[macro_export] macro_rules! ephemeral_balance_seeds_from_payer { ($payer: expr, $index: expr) => { - &[b"balance", &$payer.as_ref(), &[$index]] + &[ + $crate::pda::EPHEMERAL_BALANCE_TAG, + &$payer.as_ref(), + &[$index], + ] }; } diff --git a/src/processor/call_handler.rs b/src/processor/call_handler.rs index a2beb537..5ddd66de 100644 --- a/src/processor/call_handler.rs +++ b/src/processor/call_handler.rs @@ -1,5 +1,4 @@ use crate::args::CallHandlerArgs; -use crate::consts::EXTERNAL_CALL_HANDLER_DISCRIMINATOR; use crate::ephemeral_balance_seeds_from_payer; use crate::processor::utils::loaders::{ load_initialized_validator_fees_vault, load_owned_pda, load_pda, load_signer, @@ -91,28 +90,26 @@ pub fn process_call_handler( load_owned_pda(escrow_account, &system_program::id(), INVALID_ESCROW_OWNER)?; // deduce necessary accounts for CPI - let (accounts_meta, handler_accounts): (Vec, Vec) = - [escrow_authority_account, escrow_account] - .into_iter() - .chain(other_accounts) - .filter(|account| account.key != validator.key) - .map(|account| { - ( - // We enable only escrow to be a signer - AccountMeta { - pubkey: *account.key, - is_writable: account.is_writable, - is_signer: account.key == escrow_account.key, - }, - account.clone(), - ) - }) - .collect(); + let (accounts_meta, handler_accounts): (Vec, Vec) = other_accounts + .iter() + .chain([escrow_authority_account, escrow_account]) + .filter(|account| account.key != validator.key) + .map(|account| { + ( + // We enable only escrow to be a signer + AccountMeta { + pubkey: *account.key, + is_writable: account.is_writable, + is_signer: account.key == escrow_account.key, + }, + account.clone(), + ) + }) + .collect(); - let data = [EXTERNAL_CALL_HANDLER_DISCRIMINATOR.to_vec(), data.to_vec()].concat(); let handler_instruction = Instruction { program_id: *destination_program.key, - data, + data: args.data, accounts: accounts_meta, }; let bump_slice = &[escrow_bump]; diff --git a/src/processor/commit_state_from_buffer.rs b/src/processor/commit_state_from_buffer.rs deleted file mode 100644 index 6b328c0d..00000000 --- a/src/processor/commit_state_from_buffer.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::args::CommitStateFromBufferArgs; -use crate::processor::{process_commit_state_internal, CommitStateInternalArgs}; -use borsh::BorshDeserialize; -use solana_program::program_error::ProgramError; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -/// Commit a new state of a delegated Pda -/// -/// It is identical to [crate::processor::process_commit_state] but it takes the new state -/// from a buffer account -/// -/// Accounts: -/// -/// 0: `[signer]` the validator requesting the commit -/// 1: `[]` the delegated account -/// 2: `[writable]` the PDA storing the new state temporarily -/// 3: `[writable]` the PDA storing the commit record -/// 4: `[]` the delegation record -/// 5: `[writable]` the delegation metadata -/// 6: `[]` the buffer account storing the data to be committed -/// 7: `[]` the validator fees vault -/// 8: `[]` the program config account -/// 9: `[]` the system program -/// -/// Requirements: -/// -/// - delegation record is initialized -/// - delegation metadata is initialized -/// - validator fees vault is initialized -/// - program config is initialized -/// - commit state is uninitialized -/// - commit record is uninitialized -/// - delegated account holds at least the lamports indicated in the delegation record -/// - account was not committed at a later slot -/// -/// Steps: -/// 1. Check that the pda is delegated -/// 2. Init a new PDA to store the new state -/// 3. Copy the new state to the new PDA -/// 4. Init a new PDA to store the record of the new state commitment -pub fn process_commit_state_from_buffer( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: &[u8], -) -> ProgramResult { - let args = CommitStateFromBufferArgs::try_from_slice(data)?; - - let commit_record_lamports = args.lamports; - let commit_record_nonce = args.nonce; - let allow_undelegation = args.allow_undelegation; - - let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, state_buffer_account, validator_fees_vault, program_config_account, system_program] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - let state = state_buffer_account.try_borrow_data()?; - let commit_state_bytes: &[u8] = *state; - - let commit_args = CommitStateInternalArgs { - commit_state_bytes, - commit_record_lamports, - commit_record_nonce, - allow_undelegation, - validator, - delegated_account, - commit_state_account, - commit_record_account, - delegation_record_account, - delegation_metadata_account, - validator_fees_vault, - program_config_account, - system_program, - }; - process_commit_state_internal(commit_args) -} diff --git a/src/processor/delegate.rs b/src/processor/delegate.rs deleted file mode 100644 index 7331f398..00000000 --- a/src/processor/delegate.rs +++ /dev/null @@ -1,178 +0,0 @@ -use borsh::BorshDeserialize; -use solana_program::msg; -use solana_program::program_error::ProgramError; -use solana_program::sysvar::Sysvar; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, -}; - -use crate::args::DelegateArgs; -use crate::consts::DEFAULT_VALIDATOR_IDENTITY; -use crate::processor::utils::curve::is_on_curve; -use crate::processor::utils::loaders::{ - load_owned_pda, load_pda, load_program, load_signer, load_uninitialized_pda, -}; -use crate::processor::utils::pda::create_pda; -use crate::state::{DelegationMetadata, DelegationRecord}; -use crate::{ - delegate_buffer_seeds_from_delegated_account, delegation_metadata_seeds_from_delegated_account, - delegation_record_seeds_from_delegated_account, -}; - -/// Delegates an account -/// -/// Accounts: -/// 0: `[signer]` the account paying for the transaction -/// 1: `[signer]` the account to delegate -/// 2: `[]` the owner of the account to delegate -/// 3: `[writable]` the buffer account we use to temporarily store the account data -/// during owner change -/// 4: `[writable]` the delegation record account -/// 5: `[writable]` the delegation metadata account -/// 6: `[]` the system program -/// -/// Requirements: -/// -/// - delegation buffer is initialized -/// - delegation record is uninitialized -/// - delegation metadata is uninitialized -/// -/// Steps: -/// 1. Checks that the account is owned by the delegation program, that the buffer is initialized and derived correctly from the PDA -/// - Also checks that the delegated_account is a signer (enforcing that the instruction is being called from CPI) & other constraints -/// 2. Copies the data from the buffer into the original account -/// 3. Creates a Delegation Record to store useful information about the delegation event -/// 4. Creates a Delegated Account Seeds to store the seeds used to derive the delegate account. Needed for undelegation. -/// -/// Usage: -/// -/// This instruction is meant to be called via CPI with the owning program signing for the -/// delegated account. -pub fn process_delegate( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: &[u8], -) -> ProgramResult { - let [payer, delegated_account, owner_program, delegate_buffer_account, delegation_record_account, delegation_metadata_account, system_program] = - accounts - else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - - let args = DelegateArgs::try_from_slice(data)?; - - load_owned_pda(delegated_account, &crate::id(), "delegated account")?; - load_program(system_program, system_program::id(), "system program")?; - - msg!("Delegating: {}", delegated_account.key); - - // Validate seeds if the delegate account is not on curve, i.e. is a PDA - // If the owner is the system program, we check if the account is derived from the delegation program, - // allowing delegation of escrow accounts - if !is_on_curve(delegated_account.key) { - let seeds_to_validate: Vec<&[u8]> = args.seeds.iter().map(|v| v.as_slice()).collect(); - let program_id = if owner_program.key.eq(&system_program::id()) { - crate::id() - } else { - *owner_program.key - }; - let (derived_pda, _) = - Pubkey::find_program_address(seeds_to_validate.as_ref(), &program_id); - - if derived_pda.ne(delegated_account.key) { - msg!( - "Expected delegated PDA to be {}, but got {}", - derived_pda, - delegated_account.key - ); - return Err(ProgramError::InvalidSeeds); - } - } - - // Check that the buffer PDA is initialized and derived correctly from the PDA - load_pda( - delegate_buffer_account, - delegate_buffer_seeds_from_delegated_account!(delegated_account.key), - owner_program.key, - true, - "delegate buffer", - )?; - - // Check that the delegation record PDA is uninitialized - let delegation_record_bump = load_uninitialized_pda( - delegation_record_account, - delegation_record_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - true, - "delegation record", - )?; - - // Check that the delegation metadata PDA is uninitialized - let delegation_metadata_bump = load_uninitialized_pda( - delegation_metadata_account, - delegation_metadata_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - true, - "delegation metadata", - )?; - - // Check that payer and delegated_account are signers, this ensures the instruction is being called from CPI - load_signer(payer, "payer")?; - load_signer(delegated_account, "delegated account")?; - - // Initialize the delegation record PDA - create_pda( - delegation_record_account, - &crate::id(), - DelegationRecord::size_with_discriminator(), - delegation_record_seeds_from_delegated_account!(delegated_account.key), - delegation_record_bump, - system_program, - payer, - )?; - - // Initialize the delegation record - let delegation_record = DelegationRecord { - owner: *owner_program.key, - authority: args.validator.unwrap_or(DEFAULT_VALIDATOR_IDENTITY), - commit_frequency_ms: args.commit_frequency_ms as u64, - delegation_slot: solana_program::clock::Clock::get()?.slot, - lamports: delegated_account.lamports(), - }; - let mut delegation_record_data = delegation_record_account.try_borrow_mut_data()?; - delegation_record.to_bytes_with_discriminator(&mut delegation_record_data)?; - - // Initialize the account seeds PDA - let mut delegation_metadata_bytes = vec![]; - let delegation_metadata = DelegationMetadata { - seeds: args.seeds, - last_update_nonce: 0, - is_undelegatable: false, - rent_payer: *payer.key, - }; - delegation_metadata.to_bytes_with_discriminator(&mut delegation_metadata_bytes)?; - - // Initialize the delegation metadata PDA - create_pda( - delegation_metadata_account, - &crate::id(), - delegation_metadata_bytes.len(), - delegation_metadata_seeds_from_delegated_account!(delegated_account.key), - delegation_metadata_bump, - system_program, - payer, - )?; - - // Copy the seeds to the delegated metadata PDA - let mut delegation_metadata_data = delegation_metadata_account.try_borrow_mut_data()?; - delegation_metadata_data.copy_from_slice(&delegation_metadata_bytes); - - // Copy the data from the buffer into the original account - if !delegate_buffer_account.data_is_empty() { - let mut delegated_data = delegated_account.try_borrow_mut_data()?; - let delegate_buffer_data = delegate_buffer_account.try_borrow_data()?; - (*delegated_data).copy_from_slice(&delegate_buffer_data); - } - - Ok(()) -} diff --git a/src/processor/commit_state.rs b/src/processor/fast/commit_state.rs similarity index 61% rename from src/processor/commit_state.rs rename to src/processor/fast/commit_state.rs index 3cd28521..e2b7c85d 100644 --- a/src/processor/commit_state.rs +++ b/src/processor/fast/commit_state.rs @@ -1,21 +1,27 @@ +use borsh::BorshDeserialize; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::{self, pubkey_eq}; +use pinocchio::seeds; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use pinocchio_log::log; +use pinocchio_system::instructions as system; + use crate::args::CommitStateArgs; use crate::error::DlpError; -use crate::processor::utils::loaders::{ - load_initialized_delegation_metadata, load_initialized_delegation_record, - load_initialized_validator_fees_vault, load_owned_pda, load_program, load_program_config, - load_signer, load_uninitialized_pda, +use crate::pda; +use crate::processor::fast::utils::{ + pda::create_pda, + requires::{ + require_initialized_delegation_metadata, require_initialized_delegation_record, + require_initialized_validator_fees_vault, require_owned_pda, require_program_config, + require_signer, require_uninitialized_pda, + }, }; -use crate::processor::utils::pda::create_pda; use crate::state::{CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig}; -use crate::{ - commit_record_seeds_from_delegated_account, commit_state_seeds_from_delegated_account, -}; -use borsh::BorshDeserialize; -use solana_program::program::invoke; -use solana_program::program_error::ProgramError; -use solana_program::system_instruction::transfer; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; -use solana_program::{msg, system_program}; + +use super::to_pinocchio_program_error; /// Commit a new state of a delegated PDA /// @@ -29,7 +35,6 @@ use solana_program::{msg, system_program}; /// 5: `[writable]` the delegation metadata /// 6: `[]` the validator fees vault /// 7: `[]` the program config account -/// 8: `[]` the system program /// /// Requirements: /// @@ -52,14 +57,14 @@ pub fn process_commit_state( accounts: &[AccountInfo], data: &[u8], ) -> ProgramResult { - let args = CommitStateArgs::try_from_slice(data)?; + let args = CommitStateArgs::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?; let commit_state_bytes: &[u8] = args.data.as_ref(); let commit_record_lamports = args.lamports; let commit_record_nonce = args.nonce; let allow_undelegation = args.allow_undelegation; - let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, program_config_account, system_program] = + let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, program_config_account, _system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -78,27 +83,25 @@ pub fn process_commit_state( delegation_metadata_account, validator_fees_vault, program_config_account, - system_program, }; process_commit_state_internal(commit_args) } /// Arguments for the commit state internal function -pub(crate) struct CommitStateInternalArgs<'a, 'info> { +pub(crate) struct CommitStateInternalArgs<'a> { pub(crate) commit_state_bytes: &'a [u8], pub(crate) commit_record_lamports: u64, pub(crate) commit_record_nonce: u64, pub(crate) allow_undelegation: bool, - pub(crate) validator: &'a AccountInfo<'info>, - pub(crate) delegated_account: &'a AccountInfo<'info>, - pub(crate) commit_state_account: &'a AccountInfo<'info>, - pub(crate) commit_record_account: &'a AccountInfo<'info>, - pub(crate) delegation_record_account: &'a AccountInfo<'info>, - pub(crate) delegation_metadata_account: &'a AccountInfo<'info>, - pub(crate) validator_fees_vault: &'a AccountInfo<'info>, - pub(crate) program_config_account: &'a AccountInfo<'info>, - pub(crate) system_program: &'a AccountInfo<'info>, + pub(crate) validator: &'a AccountInfo, + pub(crate) delegated_account: &'a AccountInfo, + pub(crate) commit_state_account: &'a AccountInfo, + pub(crate) commit_record_account: &'a AccountInfo, + pub(crate) delegation_record_account: &'a AccountInfo, + pub(crate) delegation_metadata_account: &'a AccountInfo, + pub(crate) validator_fees_vault: &'a AccountInfo, + pub(crate) program_config_account: &'a AccountInfo, } /// Commit a new state of a delegated Pda @@ -106,29 +109,33 @@ pub(crate) fn process_commit_state_internal( args: CommitStateInternalArgs, ) -> Result<(), ProgramError> { // Check that the origin account is delegated - load_owned_pda(args.delegated_account, &crate::id(), "delegated account")?; - load_signer(args.validator, "validator account")?; - load_initialized_delegation_record( + require_owned_pda( + args.delegated_account, + &crate::fast::ID, + "delegated account", + )?; + require_signer(args.validator, "validator account")?; + require_initialized_delegation_record( args.delegated_account, args.delegation_record_account, false, )?; - load_initialized_delegation_metadata( + require_initialized_delegation_metadata( args.delegated_account, args.delegation_metadata_account, true, )?; - load_initialized_validator_fees_vault(args.validator, args.validator_fees_vault, false)?; - load_program(args.system_program, system_program::id(), "system program")?; + require_initialized_validator_fees_vault(args.validator, args.validator_fees_vault, false)?; // Read delegation metadata let mut delegation_metadata_data = args.delegation_metadata_account.try_borrow_mut_data()?; let mut delegation_metadata = - DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data)?; + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data) + .map_err(to_pinocchio_program_error)?; // To preserve correct history of account updates we require sequential commits if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { - msg!( + log!( "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", args.commit_record_nonce, delegation_metadata.last_update_nonce @@ -138,40 +145,39 @@ pub(crate) fn process_commit_state_internal( // Once the account is marked as undelegatable, any subsequent commit should fail if delegation_metadata.is_undelegatable { - msg!( - "delegation metadata ({}) is already undelegated", - args.delegation_metadata_account.key - ); + log!("delegation metadata is already undelegated: "); + pubkey::log(args.delegation_metadata_account.key()); return Err(DlpError::AlreadyUndelegated.into()); } // Update delegation metadata undelegation flag delegation_metadata.is_undelegatable = args.allow_undelegation; - delegation_metadata.to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut())?; + delegation_metadata + .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) + .map_err(to_pinocchio_program_error)?; // Load delegation record let delegation_record_data = args.delegation_record_account.try_borrow_data()?; let delegation_record = - DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data)?; + DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data) + .map_err(to_pinocchio_program_error)?; // Check that the authority is allowed to commit - if !delegation_record.authority.eq(args.validator.key) - && delegation_record.authority.ne(&Pubkey::default()) + if !pubkey_eq(delegation_record.authority.as_array(), args.validator.key()) + && !pubkey_eq(delegation_record.authority.as_array(), &Pubkey::default()) { - msg!( - "validator ({}) is not the delegation authority ({})", - args.validator.key, - delegation_record.authority - ); + log!("validator is not the delegation authority. validator: "); + pubkey::log(args.validator.key()); + log!("delegation authority: "); + pubkey::log(delegation_record.authority.as_array()); return Err(DlpError::InvalidAuthority.into()); } // If there was an issue with the lamport accounting in the past, abort (this should never happen) if args.delegated_account.lamports() < delegation_record.lamports { - msg!( - "delegated account ({}) has less lamports than the delegation record indicates", - args.delegated_account.key - ); + log!( + "delegated account has less lamports than the delegation record indicates. delegation account: "); + pubkey::log(args.delegated_account.key()); return Err(DlpError::InvalidDelegatedState.into()); } @@ -184,51 +190,48 @@ pub(crate) fn process_commit_state_internal( .commit_record_lamports .checked_sub(delegation_record.lamports) .ok_or(DlpError::Overflow)?; - invoke( - &transfer( - args.validator.key, - args.commit_state_account.key, - extra_lamports, - ), - &[ - args.validator.clone(), - args.commit_state_account.clone(), - args.system_program.clone(), - ], - )?; + + system::Transfer { + from: args.validator, + to: args.commit_state_account, + lamports: extra_lamports, + } + .invoke()?; } // Load the program configuration and validate it, if any - let has_program_config = - load_program_config(args.program_config_account, delegation_record.owner, false)?; + let has_program_config = require_program_config( + args.program_config_account, + delegation_record.owner.as_array(), + false, + )?; if has_program_config { let program_config_data = args.program_config_account.try_borrow_data()?; - let program_config = - ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)?; + + let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data) + .map_err(to_pinocchio_program_error)?; if !program_config .approved_validators - .contains(args.validator.key) + .contains(&(*args.validator.key()).into()) { - msg!( - "validator ({}) is not whitelisted in the program config", - args.validator.key - ); + log!("validator is not whitelisted in the program config: "); + pubkey::log(args.validator.key()); return Err(DlpError::InvalidWhitelistProgramConfig.into()); } } // Load the uninitialized PDAs - let commit_state_bump = load_uninitialized_pda( + let commit_state_bump = require_uninitialized_pda( args.commit_state_account, - commit_state_seeds_from_delegated_account!(args.delegated_account.key), - &crate::id(), + &[pda::COMMIT_STATE_TAG, args.delegated_account.key()], + &crate::fast::ID, true, "commit state account", )?; - let commit_record_bump = load_uninitialized_pda( + let commit_record_bump = require_uninitialized_pda( args.commit_record_account, - commit_record_seeds_from_delegated_account!(args.delegated_account.key), - &crate::id(), + &[pda::COMMIT_RECORD_TAG, args.delegated_account.key()], + &crate::fast::ID, true, "commit record", )?; @@ -236,34 +239,40 @@ pub(crate) fn process_commit_state_internal( // Initialize the PDA containing the new committed state create_pda( args.commit_state_account, - &crate::id(), + &crate::fast::ID, args.commit_state_bytes.len(), - commit_state_seeds_from_delegated_account!(args.delegated_account.key), - commit_state_bump, - args.system_program, + &[Signer::from(&seeds!( + pda::COMMIT_STATE_TAG, + args.delegated_account.key(), + &[commit_state_bump] + ))], args.validator, )?; // Initialize the PDA containing the record of the committed state create_pda( args.commit_record_account, - &crate::id(), + &crate::fast::ID, CommitRecord::size_with_discriminator(), - commit_record_seeds_from_delegated_account!(args.delegated_account.key), - commit_record_bump, - args.system_program, + &[Signer::from(&seeds!( + pda::COMMIT_RECORD_TAG, + args.delegated_account.key(), + &[commit_record_bump] + ))], args.validator, )?; // Initialize the commit record let commit_record = CommitRecord { - identity: *args.validator.key, - account: *args.delegated_account.key, + identity: (*args.validator.key()).into(), + account: (*args.delegated_account.key()).into(), nonce: args.commit_record_nonce, lamports: args.commit_record_lamports, }; let mut commit_record_data = args.commit_record_account.try_borrow_mut_data()?; - commit_record.to_bytes_with_discriminator(&mut commit_record_data)?; + commit_record + .to_bytes_with_discriminator(&mut commit_record_data) + .map_err(to_pinocchio_program_error)?; // Copy the new state to the initialized PDA let mut commit_state_data = args.commit_state_account.try_borrow_mut_data()?; diff --git a/src/processor/fast/commit_state_from_buffer.rs b/src/processor/fast/commit_state_from_buffer.rs new file mode 100644 index 00000000..97440e71 --- /dev/null +++ b/src/processor/fast/commit_state_from_buffer.rs @@ -0,0 +1,46 @@ +use crate::args::CommitStateFromBufferArgs; +use crate::processor::fast::{process_commit_state_internal, CommitStateInternalArgs}; + +use borsh::BorshDeserialize; +use pinocchio::account_info::AccountInfo; +use pinocchio::program_error::ProgramError; +use pinocchio::pubkey::Pubkey; +use pinocchio::ProgramResult; + +pub fn process_commit_state_from_buffer( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, state_buffer_account, validator_fees_vault, program_config_account, _system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let args = + CommitStateFromBufferArgs::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?; + + let commit_record_lamports = args.lamports; + let commit_record_nonce = args.nonce; + let allow_undelegation = args.allow_undelegation; + + let state = state_buffer_account.try_borrow_data()?; + let commit_state_bytes: &[u8] = &state; + + let commit_args = CommitStateInternalArgs { + commit_state_bytes, + commit_record_lamports, + commit_record_nonce, + allow_undelegation, + validator, + delegated_account, + commit_state_account, + commit_record_account, + delegation_record_account, + delegation_metadata_account, + validator_fees_vault, + program_config_account, + }; + process_commit_state_internal(commit_args) +} diff --git a/src/processor/fast/delegate.rs b/src/processor/fast/delegate.rs new file mode 100644 index 00000000..07b79626 --- /dev/null +++ b/src/processor/fast/delegate.rs @@ -0,0 +1,227 @@ +use borsh::BorshDeserialize; +use pinocchio::instruction::{Seed, Signer}; +use pinocchio::pubkey::{self, pubkey_eq}; +use pinocchio::sysvars::clock::Clock; +use pinocchio::sysvars::Sysvar; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use pinocchio_log::log; + +use crate::args::DelegateArgs; +use crate::consts::DEFAULT_VALIDATOR_IDENTITY; +use crate::error::DlpError; +use crate::pda; +use crate::processor::fast::to_pinocchio_program_error; +use crate::processor::fast::utils::{pda::create_pda, requires::require_uninitialized_pda}; +use crate::processor::utils::curve::is_on_curve_fast; +use crate::state::{DelegationMetadata, DelegationRecord}; + +use super::utils::requires::{require_owned_pda, require_pda, require_signer}; + +/// Delegates an account +/// +/// Accounts: +/// 0: `[signer]` the account paying for the transaction +/// 1: `[signer]` the account to delegate +/// 2: `[]` the owner of the account to delegate +/// 3: `[writable]` the buffer account we use to temporarily store the account data +/// during owner change +/// 4: `[writable]` the delegation record account +/// 5: `[writable]` the delegation metadata account +/// +/// Requirements: +/// +/// - delegation buffer is initialized +/// - delegation record is uninitialized +/// - delegation metadata is uninitialized +/// +/// Steps: +/// 1. Checks that the account is owned by the delegation program, that the buffer is initialized and derived correctly from the PDA +/// - Also checks that the delegated_account is a signer (enforcing that the instruction is being called from CPI) & other constraints +/// 2. Copies the data from the buffer into the original account +/// 3. Creates a Delegation Record to store useful information about the delegation event +/// 4. Creates a Delegated Account Seeds to store the seeds used to derive the delegate account. Needed for undelegation. +/// +/// Usage: +/// +/// This instruction is meant to be called via CPI with the owning program signing for the +/// delegated account. +pub fn process_delegate( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let [payer, delegated_account, owner_program, delegate_buffer_account, delegation_record_account, delegation_metadata_account, _system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + require_owned_pda(delegated_account, &crate::fast::ID, "delegated account")?; + + // Check that payer and delegated_account are signers, this ensures the instruction is being called from CPI + require_signer(payer, "payer")?; + require_signer(delegated_account, "delegated account")?; + + // Check that the buffer PDA is initialized and derived correctly from the PDA + require_pda( + delegate_buffer_account, + &[pda::DELEGATE_BUFFER_TAG, delegated_account.key()], + owner_program.key(), + true, + "delegate buffer", + )?; + + // Check that the delegation record PDA is uninitialized + // TODO (snawaz): This check could be safely avoided, as create_pda would anyway fail. + // Could save considerable CU, especially in the v2 version where we will pass the bumps + let delegation_record_bump = require_uninitialized_pda( + delegation_record_account, + &[pda::DELEGATION_RECORD_TAG, delegated_account.key()], + &crate::fast::ID, + true, + "delegation record", + )?; + + // Check that the delegation metadata PDA is uninitialized + // TODO (snawaz): This check could be safely avoided, as create_pda would anyway fail. + // Could save considerable CU, especially in the v2 version where we will pass the bumps + let delegation_metadata_bump = require_uninitialized_pda( + delegation_metadata_account, + &[pda::DELEGATION_METADATA_TAG, delegated_account.key()], + &crate::fast::ID, + true, + "delegation metadata", + )?; + + let args = + DelegateArgs::try_from_slice(data).map_err(|_| ProgramError::InvalidInstructionData)?; + + // Validate seeds if the delegate account is not on curve, i.e. is a PDA + // If the owner is the system program, we check if the account is derived from the delegation program, + // allowing delegation of escrow accounts + if !is_on_curve_fast(delegated_account.key()) { + let program_id = if pubkey_eq(owner_program.key(), &pinocchio_system::ID) { + &crate::fast::ID + } else { + owner_program.key() + }; + let seeds_to_validate: &[&[u8]] = match args.seeds.len() { + 1 => &[&args.seeds[0]], + 2 => &[&args.seeds[0], &args.seeds[1]], + 3 => &[&args.seeds[0], &args.seeds[1], &args.seeds[2]], + 4 => &[ + &args.seeds[0], + &args.seeds[1], + &args.seeds[2], + &args.seeds[3], + ], + 5 => &[ + &args.seeds[0], + &args.seeds[1], + &args.seeds[2], + &args.seeds[3], + &args.seeds[4], + ], + 6 => &[ + &args.seeds[0], + &args.seeds[1], + &args.seeds[2], + &args.seeds[3], + &args.seeds[4], + &args.seeds[5], + ], + 7 => &[ + &args.seeds[0], + &args.seeds[1], + &args.seeds[2], + &args.seeds[3], + &args.seeds[4], + &args.seeds[5], + &args.seeds[6], + ], + 8 => &[ + &args.seeds[0], + &args.seeds[1], + &args.seeds[2], + &args.seeds[3], + &args.seeds[4], + &args.seeds[5], + &args.seeds[6], + &args.seeds[7], + ], + _ => return Err(DlpError::TooManySeeds.into()), + }; + let derived_pda = pubkey::find_program_address(seeds_to_validate, program_id).0; + + if !pubkey_eq(&derived_pda, delegated_account.key()) { + log!("Expected delegated PDA to be: "); + pubkey::log(&derived_pda); + log!("but got: "); + pubkey::log(delegated_account.key()); + return Err(ProgramError::InvalidSeeds); + } + } + + create_pda( + delegation_record_account, + &crate::fast::ID, + DelegationRecord::size_with_discriminator(), + &[Signer::from(&[ + Seed::from(pda::DELEGATION_RECORD_TAG), + Seed::from(delegated_account.key()), + Seed::from(&[delegation_record_bump]), + ])], + payer, + )?; + + // Initialize the delegation record + let delegation_record = DelegationRecord { + owner: (*owner_program.key()).into(), + authority: args.validator.unwrap_or(DEFAULT_VALIDATOR_IDENTITY), + commit_frequency_ms: args.commit_frequency_ms as u64, + delegation_slot: Clock::get()?.slot, + lamports: delegated_account.lamports(), + }; + + let mut delegation_record_data = delegation_record_account.try_borrow_mut_data()?; + delegation_record + .to_bytes_with_discriminator(&mut delegation_record_data) + .map_err(to_pinocchio_program_error)?; + + let delegation_metadata = DelegationMetadata { + seeds: args.seeds, + last_update_nonce: 0, + is_undelegatable: false, + rent_payer: (*payer.key()).into(), + }; + + // Initialize the delegation metadata PDA + create_pda( + delegation_metadata_account, + &crate::fast::ID, + delegation_metadata.serialized_size(), + &[Signer::from(&[ + Seed::from(pda::DELEGATION_METADATA_TAG), + Seed::from(delegated_account.key()), + Seed::from(&[delegation_metadata_bump]), + ])], + payer, + )?; + + // Copy the seeds to the delegated metadata PDA + let mut delegation_metadata_data = delegation_metadata_account.try_borrow_mut_data()?; + delegation_metadata + .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) + .map_err(to_pinocchio_program_error)?; + + // Copy the data from the buffer into the original account + if !delegate_buffer_account.data_is_empty() { + let mut delegated_data = delegated_account.try_borrow_mut_data()?; + let delegate_buffer_data = delegate_buffer_account.try_borrow_data()?; + (*delegated_data).copy_from_slice(&delegate_buffer_data); + } + + Ok(()) +} diff --git a/src/processor/finalize.rs b/src/processor/fast/finalize.rs similarity index 69% rename from src/processor/finalize.rs rename to src/processor/fast/finalize.rs index 0f5808e8..a38acaf1 100644 --- a/src/processor/finalize.rs +++ b/src/processor/fast/finalize.rs @@ -1,15 +1,19 @@ +use pinocchio::account_info::AccountInfo; +use pinocchio::program_error::ProgramError; +use pinocchio::pubkey::{pubkey_eq, Pubkey}; +use pinocchio::ProgramResult; +use pinocchio_log::log; + use crate::error::DlpError; -use crate::processor::utils::loaders::{ - is_uninitialized_account, load_initialized_commit_record, load_initialized_commit_state, - load_initialized_delegation_metadata, load_initialized_delegation_record, - load_initialized_validator_fees_vault, load_owned_pda, load_program, load_signer, +use crate::processor::fast::utils::pda::close_pda; +use crate::processor::fast::utils::requires::{ + is_uninitialized_account, require_initialized_commit_record, require_initialized_commit_state, + require_initialized_delegation_metadata, require_initialized_delegation_record, + require_initialized_validator_fees_vault, require_owned_pda, require_signer, }; -use crate::processor::utils::pda::close_pda; use crate::state::{CommitRecord, DelegationMetadata, DelegationRecord}; -use solana_program::program_error::ProgramError; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey, system_program, -}; + +use super::to_pinocchio_program_error; /// Finalize a committed state, after validation, to a delegated account /// @@ -22,7 +26,6 @@ use solana_program::{ /// 4: `[writable]` the delegation record account /// 5: `[writable]` the delegation metadata account /// 6: `[writable]` the validator fees vault account -/// 7: `[]` the system program /// /// Requirements: /// @@ -50,54 +53,59 @@ pub fn process_finalize( accounts: &[AccountInfo], _data: &[u8], ) -> ProgramResult { - let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, system_program] = + let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, _system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - load_signer(validator, "validator")?; - load_owned_pda(delegated_account, &crate::id(), "delegated account")?; - load_initialized_delegation_record(delegated_account, delegation_record_account, true)?; - load_initialized_delegation_metadata(delegated_account, delegation_metadata_account, true)?; - load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; - load_program(system_program, system_program::id(), "system program")?; - let load_cs = load_initialized_commit_state(delegated_account, commit_state_account, true); - let load_cr = load_initialized_commit_record(delegated_account, commit_record_account, true); + require_signer(validator, "validator")?; + require_owned_pda(delegated_account, &crate::fast::ID, "delegated account")?; + require_initialized_delegation_record(delegated_account, delegation_record_account, true)?; + require_initialized_delegation_metadata(delegated_account, delegation_metadata_account, true)?; + require_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; + + let require_cs = + require_initialized_commit_state(delegated_account, commit_state_account, true); + let require_cr = + require_initialized_commit_record(delegated_account, commit_record_account, true); // Since finalize instructions are typically bundled, we return without error // if there is nothing to be finalized, so that correct finalizes are executed if let (Err(ProgramError::InvalidAccountOwner), Err(ProgramError::InvalidAccountOwner)) = - (&load_cs, &load_cr) + (&require_cs, &require_cr) { if is_uninitialized_account(commit_state_account) && is_uninitialized_account(commit_record_account) { - msg!("No state to be finalized. Skipping finalize."); + log!("No state to be finalized. Skipping finalize."); return Ok(()); } } - load_cs?; - load_cr?; + require_cs?; + require_cr?; // Load delegation metadata let mut delegation_metadata_data = delegation_metadata_account.try_borrow_mut_data()?; let mut delegation_metadata = - DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data)?; + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data) + .map_err(to_pinocchio_program_error)?; let mut delegation_record_data = delegation_record_account.try_borrow_mut_data()?; let delegation_record = - DelegationRecord::try_from_bytes_with_discriminator_mut(&mut delegation_record_data)?; + DelegationRecord::try_from_bytes_with_discriminator_mut(&mut delegation_record_data) + .map_err(to_pinocchio_program_error)?; // Load commit record let commit_record_data = commit_record_account.try_borrow_data()?; - let commit_record = CommitRecord::try_from_bytes_with_discriminator(&commit_record_data)?; + let commit_record = CommitRecord::try_from_bytes_with_discriminator(&commit_record_data) + .map_err(to_pinocchio_program_error)?; // Check that the commit record is the right one - if !commit_record.account.eq(delegated_account.key) { + if !pubkey_eq(commit_record.account.as_array(), delegated_account.key()) { return Err(DlpError::InvalidDelegatedAccount.into()); } - if !commit_record.identity.eq(validator.key) { + if !pubkey_eq(commit_record.identity.as_array(), validator.key()) { return Err(DlpError::InvalidReimbursementAccount.into()); } @@ -112,7 +120,9 @@ pub fn process_finalize( // Update the delegation metadata delegation_metadata.last_update_nonce = commit_record.nonce; - delegation_metadata.to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut())?; + delegation_metadata + .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) + .map_err(to_pinocchio_program_error)?; // Update the delegation record delegation_record.lamports = delegated_account.lamports(); @@ -121,7 +131,7 @@ pub fn process_finalize( let commit_state_data = commit_state_account.try_borrow_data()?; // Copying the new commit state to the delegated account - delegated_account.realloc(commit_state_data.len(), false)?; + delegated_account.resize(commit_state_data.len())?; let mut delegated_account_data = delegated_account.try_borrow_mut_data()?; (*delegated_account_data).copy_from_slice(&commit_state_data); @@ -137,10 +147,10 @@ pub fn process_finalize( } /// Settle the committed lamports to the delegated account -fn settle_lamports_balance<'a, 'info>( - delegated_account: &'a AccountInfo<'info>, - commit_state_account: &'a AccountInfo<'info>, - validator_fees_vault: &'a AccountInfo<'info>, +fn settle_lamports_balance( + delegated_account: &AccountInfo, + commit_state_account: &AccountInfo, + validator_fees_vault: &AccountInfo, delegation_record_lamports: u64, commit_record_lamports: u64, ) -> Result<(), ProgramError> { @@ -163,11 +173,11 @@ fn settle_lamports_balance<'a, 'info>( std::cmp::Ordering::Equal => return Ok(()), }; - **transfer_source.try_borrow_mut_lamports()? = transfer_source + *transfer_source.try_borrow_mut_lamports()? = transfer_source .lamports() .checked_sub(transfer_lamports) .ok_or(DlpError::Overflow)?; - **transfer_destination.try_borrow_mut_lamports()? = transfer_destination + *transfer_destination.try_borrow_mut_lamports()? = transfer_destination .lamports() .checked_add(transfer_lamports) .ok_or(DlpError::Overflow)?; diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs new file mode 100644 index 00000000..84649f40 --- /dev/null +++ b/src/processor/fast/mod.rs @@ -0,0 +1,18 @@ +mod commit_state; +mod commit_state_from_buffer; +mod delegate; +mod finalize; +mod undelegate; +mod utils; + +pub use commit_state::*; +pub use commit_state_from_buffer::*; +pub use delegate::*; +pub use finalize::*; +pub use undelegate::*; + +pub fn to_pinocchio_program_error( + error: solana_program::program_error::ProgramError, +) -> pinocchio::program_error::ProgramError { + u64::from(error).into() +} diff --git a/src/processor/undelegate.rs b/src/processor/fast/undelegate.rs similarity index 59% rename from src/processor/undelegate.rs rename to src/processor/fast/undelegate.rs index e4072c7f..5c237639 100644 --- a/src/processor/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -1,25 +1,35 @@ +use pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::{pubkey_eq, Pubkey}, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use pinocchio::{pubkey, seeds}; +use pinocchio_log::log; +use pinocchio_system::instructions as system; + use crate::consts::{EXTERNAL_UNDELEGATE_DISCRIMINATOR, RENT_FEES_PERCENTAGE}; use crate::error::DlpError; -use crate::processor::utils::loaders::{ - load_initialized_delegation_metadata, load_initialized_delegation_record, - load_initialized_protocol_fees_vault, load_initialized_validator_fees_vault, load_owned_pda, - load_program, load_signer, load_uninitialized_pda, +use crate::pda; +use crate::processor::fast::utils::{ + pda::{close_pda, close_pda_with_fees, create_pda}, + requires::require_uninitialized_pda, }; -use crate::processor::utils::pda::{close_pda, close_pda_with_fees, create_pda}; use crate::state::{DelegationMetadata, DelegationRecord}; -use crate::{ - commit_record_seeds_from_delegated_account, commit_state_seeds_from_delegated_account, - undelegate_buffer_seeds_from_delegated_account, -}; -use borsh::to_vec; -use solana_program::instruction::{AccountMeta, Instruction}; -use solana_program::msg; -use solana_program::program::{invoke, invoke_signed}; -use solana_program::program_error::ProgramError; -use solana_program::rent::Rent; -use solana_program::system_instruction::transfer; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, + +#[cfg(feature = "log-cost")] +use crate::compute; + +use super::{ + to_pinocchio_program_error, + utils::requires::{ + require_initialized_delegation_metadata, require_initialized_delegation_record, + require_initialized_protocol_fees_vault, require_initialized_validator_fees_vault, + require_owned_pda, require_signer, + }, }; /// Undelegate a delegated account @@ -37,7 +47,7 @@ use solana_program::{ /// 8: `[]` the rent reimbursement account /// 9: `[writable]` the protocol fees vault account /// 10: `[writable]` the validator fees vault account -/// 11: `[]` the system program +/// 11: `[]` the system program (TODO (snawaz): soon to be removed from the requirement) /// /// Requirements: /// @@ -76,26 +86,25 @@ pub fn process_undelegate( }; // Check accounts - load_signer(validator, "validator")?; - load_owned_pda(delegated_account, &crate::id(), "delegated account")?; - load_initialized_delegation_record(delegated_account, delegation_record_account, true)?; - load_initialized_delegation_metadata(delegated_account, delegation_metadata_account, true)?; - load_initialized_protocol_fees_vault(fees_vault, true)?; - load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; - load_program(system_program, system_program::id(), "system program")?; + require_signer(validator, "validator")?; + require_owned_pda(delegated_account, &crate::fast::ID, "delegated account")?; + require_initialized_delegation_record(delegated_account, delegation_record_account, true)?; + require_initialized_delegation_metadata(delegated_account, delegation_metadata_account, true)?; + require_initialized_protocol_fees_vault(fees_vault, true)?; + require_initialized_validator_fees_vault(validator, validator_fees_vault, true)?; // Make sure there is no pending commits to be finalized before this call - load_uninitialized_pda( + require_uninitialized_pda( commit_state_account, - commit_state_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), + &[pda::COMMIT_STATE_TAG, delegated_account.key()], + &crate::fast::ID, false, "commit state", )?; - load_uninitialized_pda( + require_uninitialized_pda( commit_record_account, - commit_record_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), + &[pda::COMMIT_RECORD_TAG, delegated_account.key()], + &crate::fast::ID, false, "commit record", )?; @@ -103,39 +112,40 @@ pub fn process_undelegate( // Load delegation record let delegation_record_data = delegation_record_account.try_borrow_data()?; let delegation_record = - DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data)?; + DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data) + .map_err(to_pinocchio_program_error)?; // Check passed owner and owner stored in the delegation record match - if !delegation_record.owner.eq(owner_program.key) { - msg!( - "Expected delegation record owner to be {}, but got {}", - delegation_record.owner, - owner_program.key - ); + if !pubkey_eq(delegation_record.owner.as_array(), owner_program.key()) { + log!("Expected delegation record owner to be : "); + pubkey::log(delegation_record.owner.as_array()); + log!("but got : "); + pubkey::log(owner_program.key()); return Err(ProgramError::InvalidAccountOwner); } // Load delegated account metadata let delegation_metadata_data = delegation_metadata_account.try_borrow_data()?; let delegation_metadata = - DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data)?; + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data) + .map_err(to_pinocchio_program_error)?; // Check if the delegated account is undelegatable if !delegation_metadata.is_undelegatable { - msg!( - "delegation metadata ({}) indicates the account is not undelegatable", - delegation_metadata_account.key - ); + log!("delegation metadata indicates the account is not undelegatable : "); + pubkey::log(delegation_metadata_account.key()); return Err(DlpError::NotUndelegatable.into()); } // Check if the rent payer is correct - if !delegation_metadata.rent_payer.eq(rent_reimbursement.key) { - msg!( - "Expected rent payer to be {}, but got {}", - delegation_metadata.rent_payer, - rent_reimbursement.key - ); + if !pubkey_eq( + delegation_metadata.rent_payer.as_array(), + rent_reimbursement.key(), + ) { + log!("Expected rent payer to be : "); + pubkey::log(delegation_metadata.rent_payer.as_array()); + log!("but got : "); + pubkey::log(rent_reimbursement.key()); return Err(DlpError::InvalidReimbursementAddressForDelegationRent.into()); } @@ -146,7 +156,9 @@ pub fn process_undelegate( // If there is no program to call CPI to, we can just assign the owner back and we're done if delegated_account.data_is_empty() { // TODO - we could also do this fast-path if the data was non-empty but zeroed-out - delegated_account.assign(owner_program.key); + unsafe { + delegated_account.assign(owner_program.key()); + } process_delegation_cleanup( delegation_record_account, delegation_metadata_account, @@ -158,22 +170,24 @@ pub fn process_undelegate( } // Initialize the undelegation buffer PDA - let undelegate_buffer_seeds: &[&[u8]] = - undelegate_buffer_seeds_from_delegated_account!(delegated_account.key); - let undelegate_buffer_bump: u8 = load_uninitialized_pda( + + let undelegate_buffer_bump: u8 = require_uninitialized_pda( undelegate_buffer_account, - undelegate_buffer_seeds, - &crate::id(), + &[pda::UNDELEGATE_BUFFER_TAG, delegated_account.key()], + &crate::fast::ID, true, "undelegate buffer", )?; + create_pda( undelegate_buffer_account, - &crate::id(), + &crate::fast::ID, delegated_account.data_len(), - undelegate_buffer_seeds, - undelegate_buffer_bump, - system_program, + &[Signer::from(&seeds!( + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.key(), + &[undelegate_buffer_bump] + ))], validator, )?; @@ -181,18 +195,17 @@ pub fn process_undelegate( (*undelegate_buffer_account.try_borrow_mut_data()?) .copy_from_slice(&delegated_account.try_borrow_data()?); - // Generate the ephemeral balance PDA's signer seeds - let undelegate_buffer_bump_slice = &[undelegate_buffer_bump]; - let undelegate_buffer_signer_seeds = - [undelegate_buffer_seeds, &[undelegate_buffer_bump_slice]].concat(); - // Call a CPI to the owner program to give it back the new state process_undelegation_with_cpi( validator, delegated_account, owner_program, undelegate_buffer_account, - &undelegate_buffer_signer_seeds, + &[Signer::from(&seeds!( + pda::UNDELEGATE_BUFFER_TAG, + delegated_account.key(), + &[undelegate_buffer_bump] + ))], delegation_metadata, system_program, )?; @@ -216,33 +229,35 @@ pub fn process_undelegate( /// 3. Check state /// 4. Settle lamports balance #[allow(clippy::too_many_arguments)] -fn process_undelegation_with_cpi<'a, 'info>( - validator: &'a AccountInfo<'info>, - delegated_account: &'a AccountInfo<'info>, - owner_program: &'a AccountInfo<'info>, - undelegate_buffer_account: &'a AccountInfo<'info>, - undelegate_buffer_signer_seeds: &[&[u8]], +fn process_undelegation_with_cpi( + validator: &AccountInfo, + delegated_account: &AccountInfo, + owner_program: &AccountInfo, + undelegate_buffer_account: &AccountInfo, + undelegate_buffer_signer_seeds: &[Signer], delegation_metadata: DelegationMetadata, - system_program: &'a AccountInfo<'info>, + system_program: &AccountInfo, ) -> ProgramResult { let delegated_account_lamports_before_close = delegated_account.lamports(); close_pda(delegated_account, validator)?; // Invoke the owner program's post-undelegation IX, to give the state back to the original program let validator_lamports_before_cpi = validator.lamports(); + cpi_external_undelegate( validator, delegated_account, undelegate_buffer_account, undelegate_buffer_signer_seeds, system_program, - owner_program.key, + owner_program.key(), delegation_metadata, )?; + let validator_lamports_after_cpi = validator.lamports(); // Check that the validator lamports are exactly as expected - let delegated_account_min_rent = Rent::default().minimum_balance(delegated_account.data_len()); + let delegated_account_min_rent = Rent::get()?.minimum_balance(delegated_account.data_len()); if validator_lamports_before_cpi != validator_lamports_after_cpi .checked_add(delegated_account_min_rent) @@ -262,63 +277,64 @@ fn process_undelegation_with_cpi<'a, 'info>( let delegated_account_extra_lamports = delegated_account_lamports_before_close .checked_sub(delegated_account_min_rent) .ok_or(DlpError::Overflow)?; - invoke( - &transfer( - validator.key, - delegated_account.key, - delegated_account_extra_lamports, - ), - &[ - validator.clone(), - delegated_account.clone(), - system_program.clone(), - ], - )?; + system::Transfer { + from: validator, + to: delegated_account, + lamports: delegated_account_extra_lamports, + } + .invoke()?; Ok(()) } /// CPI to the original owner program to re-open the PDA with the new state -fn cpi_external_undelegate<'a, 'info>( - payer: &'a AccountInfo<'info>, - delegated_account: &'a AccountInfo<'info>, - undelegate_buffer_account: &'a AccountInfo<'info>, - undelegate_buffer_signer_seeds: &[&[u8]], - system_program: &'a AccountInfo<'info>, +fn cpi_external_undelegate( + payer: &AccountInfo, + delegated_account: &AccountInfo, + undelegate_buffer_account: &AccountInfo, + undelegate_buffer_signer_seeds: &[Signer], + system_program: &AccountInfo, owner_program_id: &Pubkey, delegation_metadata: DelegationMetadata, ) -> ProgramResult { - let mut data = EXTERNAL_UNDELEGATE_DISCRIMINATOR.to_vec(); - let serialized_seeds = to_vec(&delegation_metadata.seeds)?; - data.extend_from_slice(&serialized_seeds); + let data = { + // GAIN: 299 (42075 => 41776) + let mut data = Vec::with_capacity(32); + data.extend_from_slice(&EXTERNAL_UNDELEGATE_DISCRIMINATOR); + borsh::to_writer(&mut data, &delegation_metadata.seeds) + .map_err(|_| ProgramError::BorshIoError)?; + data + }; + let external_undelegate_instruction = Instruction { - program_id: *owner_program_id, - accounts: vec![ - AccountMeta::new(*delegated_account.key, false), - AccountMeta::new(*undelegate_buffer_account.key, true), - AccountMeta::new(*payer.key, true), - AccountMeta::new_readonly(*system_program.key, false), + program_id: owner_program_id, + data: &data, + accounts: &[ + AccountMeta::new(delegated_account.key(), true, false), + AccountMeta::new(undelegate_buffer_account.key(), true, true), + AccountMeta::new(payer.key(), true, true), + AccountMeta::new(system_program.key(), false, false), ], - data, }; + invoke_signed( &external_undelegate_instruction, &[ - delegated_account.clone(), - undelegate_buffer_account.clone(), - payer.clone(), - system_program.clone(), + delegated_account, + undelegate_buffer_account, + payer, + system_program, ], - &[undelegate_buffer_signer_seeds], + undelegate_buffer_signer_seeds, ) } -fn process_delegation_cleanup<'a, 'info>( - delegation_record_account: &'a AccountInfo<'info>, - delegation_metadata_account: &'a AccountInfo<'info>, - rent_reimbursement: &'a AccountInfo<'info>, - fees_vault: &'a AccountInfo<'info>, - validator_fees_vault: &'a AccountInfo<'info>, +fn process_delegation_cleanup( + delegation_record_account: &AccountInfo, + delegation_metadata_account: &AccountInfo, + rent_reimbursement: &AccountInfo, + fees_vault: &AccountInfo, + validator_fees_vault: &AccountInfo, ) -> ProgramResult { close_pda_with_fees( delegation_record_account, diff --git a/src/processor/fast/utils/mod.rs b/src/processor/fast/utils/mod.rs new file mode 100644 index 00000000..02cf2aa7 --- /dev/null +++ b/src/processor/fast/utils/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod pda; +pub(crate) mod requires; diff --git a/src/processor/fast/utils/pda.rs b/src/processor/fast/utils/pda.rs new file mode 100644 index 00000000..33abe23a --- /dev/null +++ b/src/processor/fast/utils/pda.rs @@ -0,0 +1,137 @@ +use pinocchio::account_info::AccountInfo; +use pinocchio::instruction::Signer; +use pinocchio::program_error::ProgramError; +use pinocchio::pubkey::Pubkey; +use pinocchio::sysvars::rent::Rent; +use pinocchio::sysvars::Sysvar; +use pinocchio::ProgramResult; +use pinocchio_system::instructions as system; + +/// Creates a new pda +#[inline(always)] +pub(crate) fn create_pda( + target_account: &AccountInfo, + owner: &Pubkey, + space: usize, + pda_signers: &[Signer], + payer: &AccountInfo, +) -> ProgramResult { + // Create the account manually or using the create instruction + + let rent = Rent::get()?; + if target_account.lamports().eq(&0) { + // If balance is zero, create account + system::CreateAccount { + from: payer, + to: target_account, + lamports: rent.minimum_balance(space), + space: space as u64, + owner, + } + .invoke_signed(pda_signers) + } else { + // Otherwise, if balance is nonzero: + + // 1) transfer sufficient lamports for rent exemption + let rent_exempt_balance = rent + .minimum_balance(space) + .saturating_sub(target_account.lamports()); + if rent_exempt_balance > 0 { + system::Transfer { + from: payer, + to: target_account, + lamports: rent_exempt_balance, + } + .invoke()?; + } + + // 2) allocate space for the account + system::Allocate { + account: target_account, + space: space as u64, + } + .invoke_signed(pda_signers)?; + + // 3) assign our program as the owner + system::Assign { + account: target_account, + owner, + } + .invoke_signed(pda_signers) + } +} + +/// Close PDA +#[inline(always)] +pub(crate) fn close_pda(target_account: &AccountInfo, destination: &AccountInfo) -> ProgramResult { + // Transfer tokens from the account to the destination. + unsafe { + *destination.borrow_mut_lamports_unchecked() = destination + .lamports() + .checked_add(target_account.lamports()) + .ok_or(ProgramError::ArithmeticOverflow)?; + + *target_account.borrow_mut_lamports_unchecked() = 0; + + target_account.assign(&pinocchio_system::ID); + } + + target_account.resize(0).map_err(Into::into) +} + +/// Close PDA with fees, distributing the fees to the specified addresses in sequence +/// The total fees are calculated as `fee_percentage` of the total lamports in the PDA +/// Each fee address receives fee_percentage % of the previous fee address's amount +pub(crate) fn close_pda_with_fees( + target_account: &AccountInfo, + destination: &AccountInfo, + fees_addresses: &[&AccountInfo], + fee_percentage: u8, +) -> ProgramResult { + if fees_addresses.is_empty() || fee_percentage > 100 { + return Err(ProgramError::InvalidArgument); + } + + let init_lamports = target_account.lamports(); + let total_fee_amount = target_account + .lamports() + .checked_mul(fee_percentage as u64) + .and_then(|v| v.checked_div(100)) + .ok_or(ProgramError::InsufficientFunds)?; + + let mut fees: Vec = vec![total_fee_amount; fees_addresses.len()]; + + let mut fee_amount = total_fee_amount; + for fee in fees.iter_mut().take(fees_addresses.len()).skip(1) { + fee_amount = fee_amount + .checked_mul(fee_percentage as u64) + .and_then(|v| v.checked_div(100)) + .ok_or(ProgramError::InsufficientFunds)?; + *fee = fee_amount; + } + + for i in 0..fees.len() - 1 { + fees[i] -= fees[i + 1]; + } + + for (i, &fee_address) in fees_addresses.iter().enumerate() { + unsafe { + *fee_address.borrow_mut_lamports_unchecked() = fee_address + .lamports() + .checked_add(fees[i]) + .ok_or(ProgramError::InsufficientFunds)?; + } + } + + unsafe { + *destination.borrow_mut_lamports_unchecked() = destination + .lamports() + .checked_add(init_lamports - total_fee_amount) + .ok_or(ProgramError::InsufficientFunds)?; + + *target_account.borrow_mut_lamports_unchecked() = 0; + + target_account.assign(&pinocchio_system::ID); + } + target_account.resize(0).map_err(Into::into) +} diff --git a/src/processor/fast/utils/requires.rs b/src/processor/fast/utils/requires.rs new file mode 100644 index 00000000..f4688b8e --- /dev/null +++ b/src/processor/fast/utils/requires.rs @@ -0,0 +1,337 @@ +use pinocchio::account_info::AccountInfo; +use pinocchio::program_error::ProgramError; +use pinocchio::pubkey::{pubkey_eq, Pubkey}; +use pinocchio_log::log; + +use crate::error::DlpError; +use crate::pda::{self, program_config_from_program_id, validator_fees_vault_pda_from_validator}; + +#[cfg(not(feature = "log-cost"))] +use pinocchio::pubkey; + +#[cfg(feature = "log-cost")] +mod pubkey { + pub use pinocchio::pubkey::log; + + use pinocchio::pubkey::{self, Pubkey}; + use pinocchio::syscalls::sol_remaining_compute_units; + use pinocchio_log::log; + + #[inline(always)] + pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { + let prev = unsafe { sol_remaining_compute_units() }; + let rv = pubkey::find_program_address(seeds, program_id); + let curr = unsafe { sol_remaining_compute_units() }; + log!(">> find_program_address => {} CU", prev - curr); + rv + } +} + +/// Errors if: +/// - Account is not owned by expected program. +#[inline(always)] +pub fn require_owned_pda( + info: &AccountInfo, + owner: &Pubkey, + label: &str, +) -> Result<(), ProgramError> { + if !pubkey_eq(info.owner(), owner) { + log!("Invalid account owner for {}:", label); + pubkey::log(info.key()); + return Err(ProgramError::InvalidAccountOwner); + } + Ok(()) +} + +/// Errors if: +/// - Account is not a signer. +#[inline(always)] +pub fn require_signer(info: &AccountInfo, label: &str) -> Result<(), ProgramError> { + if !info.is_signer() { + log!("Account needs to be signer {}: ", label); + pubkey::log(info.key()); + return Err(ProgramError::MissingRequiredSignature); + } + + Ok(()) +} + +/// Errors if: +/// - Address does not match PDA derived from provided seeds. +#[inline(always)] +pub fn require_pda( + info: &AccountInfo, + seeds: &[&[u8]], + program_id: &Pubkey, + is_writable: bool, + label: &str, +) -> Result { + let pda = pubkey::find_program_address(seeds, program_id); + + if !pubkey_eq(info.key(), &pda.0) { + log!("Invalid seeds for {}: ", label); + pubkey::log(info.key()); + return Err(ProgramError::InvalidSeeds); + } + + if is_writable && !info.is_writable() { + log!("Account needs to be writable. Label: {}", label); + pubkey::log(info.key()); + return Err(ProgramError::Immutable); + } + + Ok(pda.1) +} + +/// Returns true if the account is uninitialized based on the following conditions: +/// - Owner is the system program. +/// - Data is empty. +pub fn is_uninitialized_account(info: &AccountInfo) -> bool { + pubkey_eq(info.owner(), &pinocchio_system::ID) && info.data_is_empty() +} + +/// Errors if: +/// - Owner is not the system program. +/// - Data is not empty. +/// - Account is not writable. +#[inline(always)] +pub fn require_uninitialized_account( + info: &AccountInfo, + is_writable: bool, + label: &str, +) -> Result<(), ProgramError> { + if !pubkey_eq(info.owner(), &pinocchio_system::id()) { + log!( + "Invalid owner for account. Label: {}; account and owner: ", + label + ); + pubkey::log(info.key()); + pubkey::log(info.owner()); + return Err(ProgramError::InvalidAccountOwner); + } + + if !info.data_is_empty() { + log!( + "Account needs to be uninitialized. Label: {}, account: ", + label, + ); + pubkey::log(info.key()); + return Err(ProgramError::AccountAlreadyInitialized); + } + + if is_writable && !info.is_writable() { + log!("Account needs to be writable. label: {}, account: ", label); + pubkey::log(info.key()); + return Err(ProgramError::Immutable); + } + + Ok(()) +} + +/// Errors if: +/// - Address does not match PDA derived from provided seeds. +/// - Cannot load as an uninitialized account. +#[inline(always)] +pub fn require_uninitialized_pda( + info: &AccountInfo, + seeds: &[&[u8]], + program_id: &Pubkey, + is_writable: bool, + label: &str, +) -> Result { + let pda = pubkey::find_program_address(seeds, program_id); + + if !pubkey_eq(info.key(), &pda.0) { + log!("Invalid seeds for account {}: ", label); + pubkey::log(info.key()); + return Err(ProgramError::InvalidSeeds); + } + + require_uninitialized_account(info, is_writable, label)?; + Ok(pda.1) +} + +/// Errors if: +/// - Address does not match PDA derived from provided seeds. +/// - Owner is not the expected program. +/// - Account is not writable if set to writable. +pub fn require_initialized_pda( + info: &AccountInfo, + seeds: &[&[u8]], + program_id: &Pubkey, + is_writable: bool, + label: &str, +) -> Result { + let pda = pubkey::find_program_address(seeds, program_id); + if !pubkey_eq(info.key(), &pda.0) { + log!("Invalid seeds (label: {}) for account ", label); + pubkey::log(info.key()); + return Err(ProgramError::InvalidSeeds); + } + + require_owned_pda(info, program_id, label)?; + + if is_writable && !info.is_writable() { + log!("Account needs to be writable. label: {}, account: ", label); + pubkey::log(info.key()); + return Err(ProgramError::Immutable); + } + + Ok(pda.1) +} + +/// Errors if: +/// - Address does not match the expected value. +/// - Account is not executable. +#[inline(always)] +#[allow(dead_code)] +pub fn require_program(info: &AccountInfo, key: &Pubkey, label: &str) -> Result<(), ProgramError> { + if !pubkey_eq(info.key(), key) { + log!("Invalid program account {}: ", label); + pubkey::log(info.key()); + return Err(ProgramError::IncorrectProgramId); + } + + if !info.executable() { + log!("{} program is not executable: ", label); + pubkey::log(info.key()); + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} + +/// Load fee vault PDA +/// - Protocol fees vault PDA +pub fn require_initialized_protocol_fees_vault( + fees_vault: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + require_initialized_pda( + fees_vault, + &[b"fees-vault"], + &crate::fast::ID, + is_writable, + "protocol fees vault", + )?; + Ok(()) +} + +/// Load validator fee vault PDA +/// - Validator fees vault PDA must be derived from the validator pubkey +/// - Validator fees vault PDA must be initialized with the expected seeds and owner +pub fn require_initialized_validator_fees_vault( + validator: &AccountInfo, + validator_fees_vault: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + let pda = validator_fees_vault_pda_from_validator(&(*validator.key()).into()); + if !pubkey_eq(validator_fees_vault.key(), pda.as_array()) { + log!("Invalid validator fees vault PDA, expected: "); + pubkey::log(pda.as_array()); + log!("but got: "); + pubkey::log(validator_fees_vault.key()); + return Err(DlpError::InvalidAuthority.into()); + } + require_initialized_pda( + validator_fees_vault, + &[pda::VALIDATOR_FEES_VAULT_TAG, validator.key()], + &crate::fast::ID, + is_writable, + "validator fees vault", + )?; + Ok(()) +} + +/// Load program config PDA +/// - Program config PDA must be initialized with the expected seeds and owner, or not exists +pub fn require_program_config( + program_config: &AccountInfo, + program: &Pubkey, + is_writable: bool, +) -> Result { + let pda = program_config_from_program_id(&(*program).into()); + if !pubkey_eq(pda.as_array(), program_config.key()) { + log!("Invalid program config PDA, expected: "); + pubkey::log(pda.as_array()); + log!("but got: "); + pubkey::log(program_config.key()); + return Err(DlpError::InvalidAuthority.into()); + } + require_pda( + program_config, + &[pda::PROGRAM_CONFIG_TAG, program], + &crate::fast::ID, + is_writable, + "program config", + )?; + Ok(!pubkey_eq(program_config.owner(), &pinocchio_system::ID)) +} + +/// Load initialized delegation record +/// - Delegation record must be derived from the delegated account +pub fn require_initialized_delegation_record( + delegated_account: &AccountInfo, + delegation_record: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + require_initialized_pda( + delegation_record, + &[pda::DELEGATION_RECORD_TAG, delegated_account.key()], + &crate::fast::ID, + is_writable, + "delegation record", + )?; + Ok(()) +} + +/// Load initialized delegation metadata +/// - Delegation metadata must be derived from the delegated account +pub fn require_initialized_delegation_metadata( + delegated_account: &AccountInfo, + delegation_metadata: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + require_initialized_pda( + delegation_metadata, + &[pda::DELEGATION_METADATA_TAG, delegated_account.key()], + &crate::fast::ID, + is_writable, + "delegation metadata", + )?; + Ok(()) +} + +/// Load initialized commit state account +/// - Commit state account must be derived from the delegated account pubkey +pub fn require_initialized_commit_state( + delegated_account: &AccountInfo, + commit_state: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + require_initialized_pda( + commit_state, + &[pda::COMMIT_STATE_TAG, delegated_account.key()], + &crate::fast::ID, + is_writable, + "commit state", + )?; + Ok(()) +} + +/// Load initialized commit state record +/// - Commit record account must be derived from the delegated account pubkey +pub fn require_initialized_commit_record( + delegated_account: &AccountInfo, + commit_record: &AccountInfo, + is_writable: bool, +) -> Result<(), ProgramError> { + require_initialized_pda( + commit_record, + &[pda::COMMIT_RECORD_TAG, delegated_account.key()], + &crate::fast::ID, + is_writable, + "commit record", + )?; + Ok(()) +} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 553c2851..99b50dab 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,34 +1,26 @@ mod call_handler; mod close_ephemeral_balance; mod close_validator_fees_vault; -mod commit_state; -mod commit_state_from_buffer; -mod delegate; mod delegate_ephemeral_balance; -mod finalize; mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; mod set_fees_receiver; mod top_up_ephemeral_balance; -mod undelegate; mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; +pub mod fast; + pub use call_handler::*; pub use close_ephemeral_balance::*; pub use close_validator_fees_vault::*; -pub use commit_state::*; -pub use commit_state_from_buffer::*; -pub use delegate::*; pub use delegate_ephemeral_balance::*; -pub use finalize::*; pub use init_protocol_fees_vault::*; pub use init_validator_fees_vault::*; pub use protocol_claim_fees::*; pub use set_fees_receiver::*; pub use top_up_ephemeral_balance::*; -pub use undelegate::*; pub use validator_claim_fees::*; pub use whitelist_validator_for_program::*; diff --git a/src/processor/utils/curve.rs b/src/processor/utils/curve.rs index 3ae60ee1..823a864a 100644 --- a/src/processor/utils/curve.rs +++ b/src/processor/utils/curve.rs @@ -1,6 +1,26 @@ -use solana_curve25519::edwards::{validate_edwards, PodEdwardsPoint}; -use solana_program::pubkey::Pubkey; +pub fn is_on_curve_fast(key: &pinocchio::pubkey::Pubkey) -> bool { + #[cfg(not(target_os = "solana"))] + { + use solana_curve25519::edwards::{validate_edwards, PodEdwardsPoint}; + // SAFETY: the layout of pinocchio::pubkey::Pubkey and PodEdwardsPoint is identical + // so one can be casted to the other without any issue. + validate_edwards(unsafe { &*(key as *const u8 as *const PodEdwardsPoint) }) + } -pub fn is_on_curve(key: &Pubkey) -> bool { - validate_edwards(&PodEdwardsPoint(key.to_bytes())) + #[cfg(target_os = "solana")] + { + // The above unit_test_config-version works great but the following one saves 7 CUs. + // ref: https://github.com/anza-xyz/agave/blob/aa5cb43d1e/curves/curve25519/src/edwards.rs#L148-L158 + + let mut result: u8 = 0; + let ret = unsafe { + pinocchio::syscalls::sol_curve_validate_point( + 0, // 0 means Ed25519 + key.as_ptr(), + &mut result as *mut u8, + ) + }; + + ret == 0 + } } diff --git a/src/processor/utils/loaders.rs b/src/processor/utils/loaders.rs index f1bbecd9..a325da8a 100644 --- a/src/processor/utils/loaders.rs +++ b/src/processor/utils/loaders.rs @@ -1,11 +1,6 @@ use crate::error::DlpError::InvalidAuthority; -use crate::pda::{program_config_from_program_id, validator_fees_vault_pda_from_validator}; -use crate::{ - commit_record_seeds_from_delegated_account, commit_state_seeds_from_delegated_account, - delegation_metadata_seeds_from_delegated_account, - delegation_record_seeds_from_delegated_account, fees_vault_seeds, - program_config_seeds_from_program_id, validator_fees_vault_seeds_from_validator, -}; +use crate::pda::validator_fees_vault_pda_from_validator; +use crate::{fees_vault_seeds, validator_fees_vault_seeds_from_validator}; use solana_program::bpf_loader_upgradeable::UpgradeableLoaderState; use solana_program::{ account_info::AccountInfo, bpf_loader_upgradeable, msg, program_error::ProgramError, @@ -50,9 +45,9 @@ pub fn load_pda( return Err(ProgramError::InvalidSeeds); } - if !info.is_writable.eq(&is_writable) { + if is_writable && !info.is_writable { msg!("Account {} ({}) needs to be writable", label, info.key); - return Err(ProgramError::InvalidAccountData); + return Err(ProgramError::Immutable); } Ok(pda.1) @@ -101,19 +96,12 @@ pub fn load_initialized_pda( if is_writable && !info.is_writable { msg!("Account {} is not writable", info.key); - return Err(ProgramError::InvalidAccountData); + return Err(ProgramError::Immutable); } Ok(pda.1) } -/// Returns true if the account is uninitialized based on the following conditions: -/// - Owner is the system program. -/// - Data is empty. -pub fn is_uninitialized_account(info: &AccountInfo) -> bool { - info.owner.eq(&system_program::id()) && info.data_is_empty() -} - /// Errors if: /// - Owner is not the system program. /// - Data is not empty. @@ -125,7 +113,12 @@ pub fn load_uninitialized_account( label: &str, ) -> Result<(), ProgramError> { if info.owner.ne(&system_program::id()) { - msg!("Invalid owner for account: {} ({})", label, info.key); + msg!( + "Invalid owner for account: {}, account: {}, owner: {}", + label, + info.key, + info.owner + ); return Err(ProgramError::InvalidAccountOwner); } @@ -136,7 +129,7 @@ pub fn load_uninitialized_account( if is_writable && !info.is_writable { msg!("Account {} ({}) needs to be writable", label, info.key); - return Err(ProgramError::InvalidAccountData); + return Err(ProgramError::Immutable); } Ok(()) @@ -171,7 +164,7 @@ pub fn load_account( if is_writable && !info.is_writable { msg!("Account {} ({}) needs to be writable", label, info.key); - return Err(ProgramError::InvalidAccountData); + return Err(ProgramError::Immutable); } Ok(()) @@ -275,101 +268,6 @@ pub fn load_initialized_validator_fees_vault( Ok(()) } -/// Load program config PDA -/// - Program config PDA must be initialized with the expected seeds and owner, or not exists -pub fn load_program_config( - program_config: &AccountInfo, - program: Pubkey, - is_writable: bool, -) -> Result { - let pda = program_config_from_program_id(&program); - if !pda.eq(program_config.key) { - msg!( - "Invalid program config PDA, expected {} but got {}. program: {}", - pda, - program_config.key, - program - ); - return Err(InvalidAuthority.into()); - } - load_pda( - program_config, - program_config_seeds_from_program_id!(program), - &crate::id(), - is_writable, - "program config", - )?; - Ok(!program_config.owner.eq(&system_program::ID)) -} - -/// Load initialized delegation record -/// - Delegation record must be derived from the delegated account -pub fn load_initialized_delegation_record( - delegated_account: &AccountInfo, - delegation_record: &AccountInfo, - is_writable: bool, -) -> Result<(), ProgramError> { - load_initialized_pda( - delegation_record, - delegation_record_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - is_writable, - "delegation record", - )?; - Ok(()) -} - -/// Load initialized delegation metadata -/// - Delegation metadata must be derived from the delegated account -pub fn load_initialized_delegation_metadata( - delegated_account: &AccountInfo, - delegation_metadata: &AccountInfo, - is_writable: bool, -) -> Result<(), ProgramError> { - load_initialized_pda( - delegation_metadata, - delegation_metadata_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - is_writable, - "delegation metadata", - )?; - Ok(()) -} - -/// Load initialized commit state account -/// - Commit state account must be derived from the delegated account pubkey -pub fn load_initialized_commit_state( - delegated_account: &AccountInfo, - commit_state: &AccountInfo, - is_writable: bool, -) -> Result<(), ProgramError> { - load_initialized_pda( - commit_state, - commit_state_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - is_writable, - "commit state", - )?; - Ok(()) -} - -/// Load initialized commit state record -/// - Commit record account must be derived from the delegated account pubkey -pub fn load_initialized_commit_record( - delegated_account: &AccountInfo, - commit_record: &AccountInfo, - is_writable: bool, -) -> Result<(), ProgramError> { - load_initialized_pda( - commit_record, - commit_record_seeds_from_delegated_account!(delegated_account.key), - &crate::id(), - is_writable, - "commit record", - )?; - Ok(()) -} - #[cfg(test)] mod tests { use solana_program::{account_info::AccountInfo, pubkey::Pubkey, system_program}; diff --git a/src/processor/utils/pda.rs b/src/processor/utils/pda.rs index 687e37ed..c1b470de 100644 --- a/src/processor/utils/pda.rs +++ b/src/processor/utils/pda.rs @@ -115,55 +115,3 @@ pub(crate) fn close_pda<'a, 'info>( target_account.assign(&solana_program::system_program::ID); target_account.realloc(0, false).map_err(Into::into) } - -/// Close PDA with fees, distributing the fees to the specified addresses in sequence -/// The total fees are calculated as `fee_percentage` of the total lamports in the PDA -/// Each fee address receives fee_percentage % of the previous fee address's amount -pub(crate) fn close_pda_with_fees<'a, 'info>( - target_account: &'a AccountInfo<'info>, - destination: &'a AccountInfo<'info>, - fees_addresses: &[&AccountInfo<'info>], - fee_percentage: u8, -) -> ProgramResult { - if fees_addresses.is_empty() || fee_percentage > 100 { - return Err(ProgramError::InvalidArgument); - } - - let init_lamports = target_account.lamports(); - let total_fee_amount = target_account - .lamports() - .checked_mul(fee_percentage as u64) - .and_then(|v| v.checked_div(100)) - .ok_or(ProgramError::InsufficientFunds)?; - - let mut fees: Vec = vec![total_fee_amount; fees_addresses.len()]; - - let mut fee_amount = total_fee_amount; - for fee in fees.iter_mut().take(fees_addresses.len()).skip(1) { - fee_amount = fee_amount - .checked_mul(fee_percentage as u64) - .and_then(|v| v.checked_div(100)) - .ok_or(ProgramError::InsufficientFunds)?; - *fee = fee_amount; - } - - for i in 0..fees.len() - 1 { - fees[i] -= fees[i + 1]; - } - - for (i, &fee_address) in fees_addresses.iter().enumerate() { - **fee_address.lamports.borrow_mut() = fee_address - .lamports() - .checked_add(fees[i]) - .ok_or(ProgramError::InsufficientFunds)?; - } - - **destination.lamports.borrow_mut() = destination - .lamports() - .checked_add(init_lamports - total_fee_amount) - .ok_or(ProgramError::InsufficientFunds)?; - - **target_account.lamports.borrow_mut() = 0; - target_account.assign(&solana_program::system_program::ID); - target_account.realloc(0, false).map_err(Into::into) -} diff --git a/src/state/delegation_metadata.rs b/src/state/delegation_metadata.rs index 669fc774..f9b9d8a2 100644 --- a/src/state/delegation_metadata.rs +++ b/src/state/delegation_metadata.rs @@ -26,6 +26,16 @@ impl AccountWithDiscriminator for DelegationMetadata { } } +impl DelegationMetadata { + pub fn serialized_size(&self) -> usize { + AccountDiscriminator::SPACE + + 8 // last_update_nonce (u64) + + 1 // is_undelegatable (bool) + + 32 // rent_payer (Pubkey) + + (4 + self.seeds.iter().map(|s| 4 + s.len()).sum::()) // seeds (Vec>) + } +} + impl_to_bytes_with_discriminator_borsh!(DelegationMetadata); impl_try_from_bytes_with_discriminator_borsh!(DelegationMetadata); diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 6f0c832e..39989b02 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -10,7 +10,9 @@ pub enum AccountDiscriminator { } impl AccountDiscriminator { - pub const fn to_bytes(&self) -> [u8; 8] { + pub const SPACE: usize = 8; + + pub const fn to_bytes(&self) -> [u8; Self::SPACE] { let num = (*self) as u64; num.to_le_bytes() } diff --git a/tests/buffers/test_delegation.so b/tests/buffers/test_delegation.so index 4bbbeb3d7e547796532f489cfc23a77136cf6105..352b3c1a973ba5976707bc64ce145ca511b66e6b 100755 GIT binary patch delta 61911 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 PZ%l45(|P&2klyyc_9ll5 delta 61456 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

?a{g%<^rymP!0 zyfb`n+6kG#;p3WrX972xQ7*-Bo0*1>?uV|*$A)mk;j|Yc?OAhU)L1NCxb4imk~S_X zL^(#gBwlqX)SKym(cVZ*PfEDRS#i9$iC>z5?-)eBwo%UHdQzZ(aoZ|Wy!MIL!-x{} zb)nH?_jlx zl?Kxf!=S>2My2x#oa{jnGdcB_)6X=^d_mSw%U`WT-S9LIj_S5MxNA9%K79CIEUI6( z${EM;{)b^cjpJ>R^|?B)TlaJv=4Y7_oP6|e#(7g;C%nHK=A)eOFyfEvp1yZhE1pml zd}wOBqU$e4{)X0xaYMpiZ3O>3gpT1>DeBEZhv8GcW^4p!3jg7jR4x#lR!#kFvYw_k zz4-HPf#y$_iqzF~gW{`LI3`|irkqP|WHIC`U`N{&nvD|GghgJu!WV>mtm1qf5%_I% z;;QzmjQ>t3QVH}+BxuY^9ky{*iop&$Y)QXuhdod-*xunes>8Um5{zC{d`%W<`;QUE z%i*Ohh|ty}1JUTvcv$%z3G(aA)$j0fh*K9YRBesekG5Eaask}J%63&G0Ixw%F#0w= z?fi~Y`s`>huc1NUyJAt!*X!GzcGaoacXVaAvw`~H(1Glv^d)}j*D$Qd(JP(Mm&M5Z zWls6N(K0{JDc>s^lD{LC_+OA-BQ7xb(}*%k-V1Tlu48K_-O{JZQp<4B-Z@ zrqLobZbs15)yo)7lggN2{G=*Jz;>j}?MqX?3Bn@=QfTGtBtA&t>2r%=Ru?*y&NajO zE;P(*Dt8^zeWEcuZULWlp%29r%epHnMe1nT{GK}9qgLiqW_`mtJ~Ipt3N>z9{#aA!^@|G>L~s*2&?p0sx_ty)shHp&(pPkd}fCbaw9;hxnb-q&AJ z(kk-=g881zb z2&2@N!8^e;-lnHZ$?6i_0k28U3R8+DeHY2EmMX}K^Fd{%At+*nX_L ztglR?{VpzVoJwK+Ny0IyNOtR_q6!E?F%oXOj4MIF`fO3`@K)F^8Ba1v zd^fNTCwP(3i?SomFW`+gf$A#)4gz>t3QD~}(P)@xDBo4`XDB?sR4OL_IjOLM@@GW7 z0uOmWqhLxYhkKwTmPy9c4M+N1HP5EQ%pjVE7qWb|G^BgmMV?71vUz-|`Be}dF+i$S zx61Jd7Y(xaZI_(N-nPH^Er!qh=yDq93w5D1lC*;EPlL#7a6Xjwx7~}sSHOL)Y@w!7 zkmBbTcn^(}3&!Bm7H_~YKxiB_%hR|uQq_F0;g1o+>=19*B3U&~9O0-dCCGOITlugX zn<$~j4THr}el z`aTsmCBIC0)Ldye`DMzZ<|+>>Q-(J;MY2;C#=B+Go>iWNUe2HPc48$Ph9{}bNk>l# zQ~W47nB4^(Em0)@2T!^pmj#9=wcRKuRF9t2cBP!V=sOj@5X(V|{1Jxy!# zq%eW6tJ$Ufe1&|)i|0FJ!@`r|ZH;$J#QuhVc0ScD7 zFHk3*w5k{>nl`tSqG-FvsXS2C6QbyfTW@cbkp}lz=@>fKhx?z^JS}LA@IbH5zj4>w zDzR#|!@sSxkN0-``9qsO;v5OVJ!s6x*ZGeks+5%|XKA0e$OJe=SgqSeeqD^&cW2T^9GqWL`-`)QzyuFbr0noZKoKdg!0=? zF-YfaKgA%8LMsjv1auF{XWJ=;4&J(4HarYFf{&Un;p1Q$Zqv)9%NQ51he=n?zd>2rAm#dP@qsiUe|?HH2YrfRF0!-AT;QP)Ix50LnSh6K<8&!a zJ>5`!4j*YlZq}2ee-*Rw+n4W+(+|m4ejN{dT%Kb)~6aTI<&FO7KeC=`baPPV$+e6qzIy zh{p>K`q2fj@JHf~7ySs?zbs`>k6nTbp)}s>1OBV{_W4qX+4VilY=j9G+8tK;Q+Lln zJc3xOtfB^f_M$QH%)`VN`gv1tYVm_)ZyITnAvxpM3YxJOoFylPkY2t*P>lmbBJMDo z?6e76+7H`NZ%%Z)RggdWOj*CjP4;^AQb)INpQ`i(!+YWua1w{w z4m*^>ZM|vKU5a7HH?Ri7;~W@hXZ$^RxbPnsPlXe`=^T0kelO}~8jrtvd^C!V_Il(m z7!2s8803BOr=JS@bT#dQF46P`+E-eet^0LI;v_RJdbSBcq#zC!moSTrXW$hT!pq)luFOcks(xV zM$ARtobk?8n;}D}#sLpJl&s40WW55zqb(u>en;6hYYUG3X^uNFW4#VoDSk>{MLVg&+kL#O>62WGd${z zyqb)|fzA!b1tp92v@Wx+2yQTR(F!iHG=XB87>mCa)0)oWkLy+&JYuTq8=IuY(IOr%{k!n57(oW$*I3&7 zP9`aH&Ags~oXBl&TiD;=fCo^{q2x2&56J%34ikdQUmPq5WD+lt3OOw7OT)MvD*953 z_cY;Os{g#%5Bt(6kEnKRmwxToff*coQ%7mRUHxceuo1rD>Ip##{7?Xzgpd=gQgrJ2 zVXMO(>F`z$^`m#$j5jh`jkHr^IMg;uy>K-d_?3 z&)UUNYuBOZOj@nKM?>M6I2se=E!3tV0)N`x zz*NnL&*SKbKzFC&1Zl62Lu@=9Mocg(o=ysrXNlWsK{k+kP-W9zSb>{M_JuaBdTI*?u(reJyMTYFB9LVCG8^{s7@ zgGk31%?arXDcBKwG}JzSVCNt@(EE^LN@?K+5if#-!PH~II>|!zPnA{i;VV1ji+Xn439TRQ#zX`$y zQTNowQkmPq8dpef(`uy^w(WV8XSTP-@7;kS3$Bm$2UB-&A4;t=(qs?rRN6?2QLxfR zQi6h250i$;9^I*Wn3N*nOw~h~IC#P=cmnS(ND35ac>S47J>>MQhs!RtpN>~o1+8$G zc;()I!k(v@Yj zU6G!q8l122uP8WA!P3rc1=hJLW3oUfu->oWECuH%I7bmMHdfLk+c#EWs$6cYFqkd{ z-*$zRNx>RbiQGAVI;U3XV~5jA}q`;c(}t$^%uo zoMzgiFzc{9j(_XPLm^a(3hh5Yt5vYOD&GqFjdX-f&Stv$JsJLdSD_tK%dMRXKC0l| z3O=IX{Q|~Y!W4W&!MhcFO2KluX1p54-<-?xh^j!WU|YE>yX@f%lhTXSU>Q(BJZxE$S|Zd z{MaY2e{HX0BW}MSi0#LwVEwb#6?q>MMO&5HYE}Kc*CDyRFYCWE66?QtU6EHUFH+wS z@F(u@Rd3q+W+Q_S*?a|ITpEx`T*cU8T$$ug2MRbES8;sIG6GYsrZ~7QiuOr<7OA_M zE=uldloHj_)>th1yWwgRAFWbpy?UKc2%ff_`yjOrTaGmNF%GF7lL^BMxR0onnp6jW z`BG<`FYYDXz#yc>Mk%e$S)FI&P# zdt4;AQVgp~2&k&9DU&iUu6;37}L zrIJ=9FY?p*?@IB}sz5TQE=1F+#@vUONo0C2;)Ect&6O zq47ykuDM_vH(`5t)z$VYqR1t4~g(S8BQ%X?r+8m*yadmw%cO{J^%K;amgOKp4L>=@cW z$6@t!Kk`w{k zGw4*>_!*SEOf%^I&!FXHr1;ur_T*N&gwUUZ?V%HBGNGRa!}L2a-uOyy)_QXx-E_45 z1K$gdi4S~RkBJX_Upyv0@ICsN_`r9c~_f-(2&ZQ+zL@FJL zT$n3~gw8`EG4Dd^JcQRx1kVB*3zhR|BrWX*yXGOW&3Jv$1E{qWPpEhRJB2NteE>W2 zR1jX8Ofy`%>_#u%S%7y#j7N+IQU5uzgkJauHdXO3mSfY+Je57npgRL$dO=&K5~1rO*a-G4^bzEzA`o&hb&34=`kuve zw65U&D7}R)mNx++KME5_tz_Sn~d1)6U7NVt`-vxz*$nV#?prMeK(F40+;$sN! z-UStpA^$se!R5zjsLR{Cu!*fY9S(QR$AHDwC@7gv%kbvA9Sia3?~ehHp`kz@-vvz? z-j%TkRxiTF@^-=W1=z{iNPGcyB2&V6ZBv|$M#9@VokwpHlqSN$g*1iF3!L0SMb1l+ z^N{4Oek95L^bt%fLULym-24&5+t7VK>sa5L%3VS^1H4@2eJQDMs!}5q~jq zw-R1`k_N!~#dJQ^Bn|Q)YRty9GMDq>Nb*wfTc z(Q)T=?xo|ugx05MDh70JU4nj@b_SL#L9M=U0vdQaxfw3=^zIgjUrH0nZ*bpIJm~fC zX{cC=9T<56?=Zxao`hDOPW}SomtlJ6S1=t@vJX}-qy6F8)zlmIETdukf_luLFPy^r z3lT3AKc9dpsfR_&F?;?As9TOYUv(0idGU%f;I;zO@UI{dQ?eKCTY+dVeg$Pbd-G}7 z#Z&JyaG4ihg5E2!cu)&WT#4D%)39VEQd{~Z)L}~S2C|i?v$3b3btR5r^aO;i!t{|A zNaktg37F2)0Vkn|r&(v9f~WKMK?A1bEjY1?2G9q-g3CO6@oBKE#_UrtWHqi3yzm}+ z1ZhbN6!LWT2`J;~l#{THr-f&rnWtT`I;JRZ{}S5My85g3iNw_uB}PC#^QJXJMhT*i zYJpsy4m<%Rn386A6|13K_hLd#_$3Wq+V2c_K8>YkAns|*e*O$(AV_cg3UZ%D0X%aW z%6Re5@D?vlJq^tqd>bwyNInMtQp`>M5)w8sV2E&64sciOx5=Jl3EH5Z}mS(`%D(VTxo~23f1OF>xElr}O-Y|VF zjuIEWpaN5P<#{amek~mcT`Tdg*fKhhj`xM>WoXMO-cW`q{K;{SmC;z5(jD9YOHOr% zWS&;~LZL{#pbk?AegQFm2E;V^Lg+fg2=j)Cn8Kb4EGb`yC3)Vki|71VNxh)UbF>cx zufwuY&tcil?vVQ&mQ5|goHuxmw;!}%4lVWtOF5?deIY}nUa$mHSjg#qT8@|rzHqr5 zt-06>qMpZeE4GX&46jCvXP&24YVwA<=V=rS-+(!1c+PllXyrMpc+SA}9H%>EtVdI* z#8IE8oy4O3WEjRW_& z4R}<3D@v~dbEa}hCskl<>;W#XnfMnyg*xM9>(^q=8$2h$8=5eOX8FKno^JJm&=)X0 zNlD9FJ|Aikz zLfP=Y)^iJa2mhkuy`Z@gYcAyUJ*qG#4<%ZKEmwL&22Xc-K_O2sdP4=KFqP9g$T9c( zzy)4X>IIf+EO`a6B)uBxzsKeGF#bg^`q)dVX_G5`BpjyKqAA9OLq#oqF8=ih*j9^` z&Ee3(;d5bdxfT_Lezw6g(vA8AUll3sC{2`%Gv<01Lq@dpqB3`1t*8haz z=0|-9v{5agxC<3UK*mexmk02Y)|ar+ZDFwFC1mQ(aM*=uhem%@3q0Wc<1H4K5Y7kUU2kPx{)4iv=`OU7UH|#|N0IM@w4Y{rTBr7 zzKwFV_`vsg=W>NNG}qHs+6go9N;eN4j^vzc-uBivX{*_{(wR&!B)?59wt=`u`WRU&QcX5!$ zdtuhThjV^(sJ;0;oIdFvJHV|0E%ovah;KmS8S4cZ4QM=Lydb-QX2T1Sx+>Ed6(?Gr?=hqzC$ zb`NX=LE85hDB|hLOHjkp=YN4FjDgUu?xKDM z&(CO-mHpa2mGeCzdXz9|AAftsXK2v$;JfyWeKgsHJJaVluT^%1y3cWGdIWAggaS!R zf|5h%qR(`NrbDBF7qFK^omPteOQ z9nd-j*o)54>29LLxDT()7zPks$i=%TPqj zsj(3b!skNf69w1hGN)G7dBO!^#xFi`CIWn%N>_P81~pr(&p1&-6$q8}^@Jj7PK=%A zRE{B1(b_pDdJ8aUq|)kv1;bQ@mt7&Zi+P)$^HmZT@%KjdQ=aDG1iyV3;%)vI z-3HEjn^S#DowRVXJ0$y<=h|x{%;#L-NHE^q5mC>>i&10^_=lJuqF(iUA`Hb#o?i?x z5B1sYMBp5p4>9+rmU??=sJV!cv#_D3xhJmfcl0#B2M2DYJ>Zsbb7jO3l&qsQ??S(E zH2z<`SxHO;RHot?^cwrP2=h|cpk>YNerVi=#6Jh&2j`>BBk0O25FcYsB%R^D7&Bg8 z{To!onBz$f?2SQ=-oFB^988C}J_vsQ8%*zm;6PZ-!N4z|p$}T!r7Lh2v+25Ku*4#~ z_cut6MYPA@!B}&Dau004gqHmdO|dwwkm4SetBaNaveutKLWH>0)_n ze=L9FcSub@7|Z!+$Mee)u=SUbK2M7Sp*aED+44KY4?w&>4@tbRKqwkuPIj!(_gR6^FwmSqN}$^yvz^w)z^*~&Wbql*r>m(y1P?avr^B|w zg~8?u9@r@vf^BSn2bzYUT5Iug>LKPL8utH6I~Opgs%rtClR+dMjkC zLO{U`4;#@!1}j8)WrTqs!w_NAh?Ru4()gmm2RpTb5kzA}2-ccZ9u2l&u3jOCN~{%Q zd(pJFVz{js0ikQ3b9QppviJJk@1F02oqw&h*V<=4&e`X+C&gBclINPY9A-yH$pcIk z_W3C2L2sU7qeep!Tx{uRd9=yRwm@RO|1>)~8Zv)ziuHCuUpSwQb3tEN$(Hh@m2H5; z{Lv|PfGfYnzT$Wsc6JP4E1NS0&~b#V1vEc@itXX)v!~hdF>(XDe6(z3L&nNh^9zUJ z@1H?Qdb7!6p{)*l$rg@jekEYx6uX9F zJ!|9m;g@U;$C+#kpy@koAD3=8#XbgU{hNnt)!PNWGT|G)c;9=9HCzR)_P0}PJfx;8 z*qo~%+gD$*RUA9m20-`|54*W^6V%LA(6ld|W~0VI)0W{j*f{wbWz6<-=N@r?yPx?_ zr`Z9nun%Hzfiaf3$HM`|IVe42ynNPddY;|bEYCFUWG^(!i%gHO{uAK%vgxh#nhElL zlll5>@E8O77dTx%N$$9$-1@??(0NiiXInUrPcOJ$cALx}^-MqWeK<4paUH3Z zmzT?Rgq@uVry!&J{55*H;i($vM@*x7-s@rGPl{Ye(sQRlG0g9GOK)tE_mBus3E-jQ z2I?Wg^Mi-~DZ)%$ua+=<;0E~__`ASE(s$el`v6avcF&OSH2oX9Vx~OJoE^m0%!D0> z+ovDTgjP-NWDP!O{&Q#89X=S6quD;6Jk$JE57uxKwBTrV>?S#EE)QZ0Z!FNAaCc#^d<6OR$L(e(A2`_w8^k2e?4nh2q&N^Y{Np>gCB5< z3z=(?ybA`zoG^^;8SK)4{4>){Ym<*EYh}OLF&OU0LAz*!0n68uuCN`>(~svpb}#ndFtJnKCoF{ zZyxI;?he6c1pE(w^X;xpCGEim3_cIBd+aN=@fnz)Qu)@e*#2kawdVB_oAInXe(=h3 zwUNtDJ@AB7J<_~&PG;MmmHV4>GTRS^^*(cLzqld455NyeNprd(38z$ZL_6-Se_rm_ zeFrZkpHE6`TiiU*R8uLNLACw``Dyl@t+KmMgtYwbGi=pXsGQJO`~*6{-0~$mxfN=a z+N~va7ktS&w#g^IW;Ykx!L8L#x65CA&1?iVGrwA49XsTYzh);9yGJYR=!^2lb+mYI zv3s_{;1PSVu#%qo5}ath=I@@;udXS z#7C($D)9xz_?8lS!FoD8&(?NV2C#nJ72DAA7|qx|T1&lrSi!~@9gvu*r_#53Swm;6P=7UFTykZ@N(T%AcIW*-G6~>;s6RX3yj4gOZ0 zDDq42#!GkI=z2)u27$U6qi`vk=y7)k4VZ2}T3h34~(FK(oHR~Dg%hkrRAGlS~J4F0(=2`t)@^K-xh`44@|68&CaV@D$X!_qee zsK5%9cHte;Z*cgH{tiNc;0-c;3ro^FAkb&Ls_+xw3xpn0QsQq6(Cv@1G{BJmf2|RS z|9=iJ3<8K@z~lUXYJ|s^w!<@3?MW&ps1e|cw4dt#UjplgMU|cZDN9!v^liXR23`;R z9Rt4#e6@j1$7Jbpjn(+l6(Hbuf5kQ(xKRM48PW$<5c>kr$3U+izf~_8U}KFP0UoYu zwD{5)5QqZ;myh~b7^qg#EMV>s)C2-}^|!js z1s+)?SZ5VjKN+afmw=50I85?OCEP?;02`a66}TBZiTKhc5E$F=Fz}Uz0`&e1Zo}^b zyA1k!fhQRFHQ;Lu-0QE^Hdm$1z~c?^r7#F48#4SJ_+kTp3Ovrhm;9G3=_h1W>Ic5s zpnnqBxI=kh;|}%sn=DPK?LT};0|H~Gy93zRW{(3K3-mi+trKZcOTflH(&tm0e-yCM zeirbUYW;(cM?qi=up8Jo%>EnLSiru=aUZx0*lDN%7a0||fZkX`PXUL;37hBpEeMPO zioizwSzvwKs$nks3|Hw=U}L9!0@%0%hk%X85ldMPqfRol3{_&-gP#IlW#Hcdf6u_( zPv9E37}(fH76OaO#yrTUy%;P>H91?@U+G;RG}uiv@q5DB7Cgb$_J@zYoNZp9t^`@*k!87eFs2 z1S?Ol-Y^ZVUumeHxsXr)rbq()c+e033FwWdym9bkPt-e)6YmMLOLZzUJY|)+M6#W% znH&Qa2fvtVdRs4;@WU^@X%cmQ9OzaRsq^EWJ*uovv9Uy*JOzRkzeh#fj7_q*^_ zxW_F90^g4}fuYT-TLu>Y;(jl&c{RUTJ&jJ%_>rv)`T+Gs{k7=8}q#}I#d~0{{)mm zyLke`a{SSbRvx5woV^jSdU~e2w4+4uhJP-AAU~!Sjc^fdi zKrgnilqS`56by`$a63FhR)jgEAyS~ffxWSsKLs{+nF_FR!nhAzJuZdIK)9Jzn`BGU zH#xir?2UU&?Z<*%)HweWF=8rnBitN2*r4>Uhac6GD?jpx>iAq?XB*%M_PL@|XN0ps zFMti!6Kr|xaLK@8pX~AhiU>*TcY!e zBXX=k(~G0yCe6Jl5BqGGGNwM6)uaVo$(xR^Vq)LLCfJn@c$n8>R|cBqsR=9El{?w| zM&;v<@>I;Z7R;XMm|cF%F~SAHxm%3(Fn_hq zHwxf(+d|Bd0OmYl+hVMD-ikT11hdp`U_O7&!2ySFdQ(d74$LLO$>msIB<$+M`pg}e zE0G%W_>yyVO~8M72>S>}2?y@O`6Gnmgi|$E<4eVwK=l;7A3IDvfVoIG`XJVq2)iD_ z`aqSn_)?@Q&(4N|2zv?p31FXnv(4VM`1Lj1ac3!}=8AB4Pi- zntfPwx~wh`Y<&bfbQAUw4iL6Kit~FO!|c~MT4U5_0k2-_Lf=Lp;4SRYpz z>R;@!@8{ScLAXrV_c*rC{Q`683C#8xjcru=`T(=*DCRO@YZ2?+9~qc;g7}{d0{B__ zGiJ{*%(*h={NFLBKF1vW0&}HOV;*11d|4ClU%^wDbEh$D%RtonO#Cd?YoBA+^cj;$ zH4uF)C|6mHF9nsVKs8Kif2X0^DLp;0KGX|yqBmx@6?3Q#vtR%7P8M+!E!r0wWcy(* z^~anTh`GSu!hlWep;V7KF&J}Uh|b#nL(T?lkkD6y6%Gq_tdBKf&Rl@Gd?9AfaLn;< zXuG(s!{wJ{q&th1oJz<=RQWHXa+qCt$7+_VBx1hAIycjuB1} z_D$mXq5ee`+pfnBEiIUfg#ACj`Xph`jaVO?f!V@uxAFED1&DBgfkSgJ$6GO57h(<) z&Jr#Ywk$Gf{a+No1_EON@`Mx1us+s_IdD7Xxc>JXg#XN)SYHa6wEiy)Y%8&W^&ZT= zRhaXHr3ltLeuUZkW6b%Vgt4G>FJ}K5%w@vfpJIK2aGr39a3xFxr6_jjy$^Gqu;&4+ zuMl=Wi1iu5;aChC_#ejXeGGGjaAG~y+Zg8Dtu_M}k&??VzU5>7pb?R~$*9N&uBx(#z`yMcNCPo@k4;NlL<(yuXx z2nW+x@5o?|zJfV#V0HZ2c4C7V;rK4BFYU&h*n>H>7qj##W>1Z^@t>~=)B}+H_c%i^ zhdKE=X4e~-Q-p*6f%Q^euohp+Rt0K&DR)R1XqwUmk2 z{&PbDo?0y`(2@;MI&JnHzCg-gfm9-FV>>1&)0&D; zV87A^%yz;& ziLh-eaY#5wI8HcQV>P~1stI^}$@&5g;3DiN93`A0Tp%oM6aKaMlA|h6<4az`A;Jm5 zIl^Vaw(TOI=HD$?k1qv;K+{Vx!WqIv!j=>c;3VuL9MQRYIg%s>dBPRK_8mB&hj5T^ zoN!j-+T}=z7+7D#0bGRrgrkI0gbReFUvd9KYF7geRZwH^uh{f!lo8Fjowy)H!uGvb zpZ#~t1=i?+^Q7fQ>l-+_gK+F1))z5{rCgpE z=xYj!d9&kPtPc=Q5-t+99l`lMh+&vSkN{pCAY3GDDPV_A!hXU@!lCyJ`S~zS7zMyN z!ezp?KNuW#r_;RR}a%6Vh|%7=#L#F_&0*ERhy_Db9ON1(h$tjFqOwv z9e6ZBl~eFqt1&=j1ZFF1oTdyjMOhOhP34hTYjk0Ky)^0m4zl7s4?L31DhTxX2p4P*L{F zaa=cHKj8@B1mP^<0_7`Eyy@`K3=&8-!hXUb!X?6Xw$7`JG-cQh*uCb;XzbnV!W<=> zAY5d1E$}vDZVb*=BwQMc^^Pksr>??WCTwSoH$Y?9#$z)d;TYi@Vd-j|-$~d{IEFba zWwAgi6Sli?059PP;S}K_VQaI|9~@epNB}ol2nPs92`35X2$u+3CKw995A6=40N6t~ zKsZV`NjOKiMA$O1Rsi+X;xGvK(bP@YPdGw2K{!jeNLbR}xrft&c53t01U$YJBU~oz zm|WWfZXY6?xen_c*JE~9S&J`us{-y0nxqA1C=j+y$9f;(1Yzk0Z0`}Q$Cs=#uz{Vh zi?Ek)fN+HJ&DeiT=jy3FNer@t3xvyrM4FEc{Dez{oePLPVaH;u_uY!wz66-JzbJr<3k+N$9BIdT_fpJR z!mbXiw=Ki$1y=i?C_s@IR0wB-*g@hp%--dg-77Gs2wOYht2*NzN7mpB8N$gZ)=T$c zwywn-B%F$2z4zfT76cx_oFnXc6zh|O9gkssl(2O@)(65w5Z{0eY#T9$2sg%Nk2x$wu|RSpafSfl2;n&40^u^@`13~peEe@U z3gGPb0_GIq%r>ldY{%?MVNMdx>@etg|98A-5ODq#<~Ze-u-@`(%x=Ec4s2prrwE5K zHN86iQU(Ff=>AR3fOC?tcNf-Mc4Lkc_U$418f)V}TNChjl2{+5x(E7tDS|VE^Mnh8 zi-b#6*5XU$sz8k|RR~Mnw2z@g0W5^AgdK$4ggt`w_>xx$wD^*baFB3_aEx%0aF%dE z=P_cB%EX{T*wP)>0DN;b3$Co)tO*hpoTXS0@YEktrz^<_y! zR0m-diWMPBjOUIgce6Y(c#gucWMK`awoRLzCz@Xv+nh`@iQAX~=;S^hi0 z4#96zmYv`UT;|P#L}E>l)Y*IE>UFXfF7^_!KYiOgp5)j^ zkTi-ltVPkp+SP&`ta+jG+ZMOJWUCO1#ef9|h+ZrXEc9Z5S-}a`v`86wS&n2Y5*91P zifqzAwtSH?qS2v$bS&g<*SXsCY;(>YXBwLeK|$p(#o0^LnJ2=d)Vs5&=vtAju4Kq9o#HM z#CgK-EFa98Ec(i4)!2mXgx##=R_J`OTNQVsST9fH6$|VM&N0&xsD%=1y{rG$dj7B<-xw9U6$g ztrq1!VB-nbWG8Z}vG-2Q(H~>ZrZ5-xVs`1Ds_6d1Qu43Z!1pQUl>Xs`D3I$)?WH=w z$+?&-Kg67U2y^mrBg2tDtG~M{9LDz<4d5ZeCzvhAF=rdJJK`dLpc!*Qe>Noa9z%_* zGjFd^06Skgb=>~~@I5q92<_11d? zL;I@^RtbT|mN4dM1apb7eKpo+^!KO44tezVp9K5v)$RGrgBQTP20L&X`UKa9me%xL zfD6CngHs!poSm7PzALczK#k7>&b?h@ey}NJwfvXx5^+JXq2{m~aAsJIxsBvt53f*O zhDC_4?Nmngu^rUZBE5SIyT4Nz(MQY~Me69sUI%&itiD8Hb_g@yu8is~zIzW2WfOqw s#CPr@-NfqbN?-QG?aGio;v0LBdZ)41Ay2nx2d^2pQ>WR^9-{R8C#bsU?*IS* diff --git a/tests/integration/Cargo.lock b/tests/integration/Cargo.lock index 9cdc153d..662482fd 100644 --- a/tests/integration/Cargo.lock +++ b/tests/integration/Cargo.lock @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060549fc24e8d948bf788e4b26a1afa24927ba3d1f02570324950c8d087d1bb4" +checksum = "625e231ca030fde0126389693a393baf1d1f9f764e7417185e9f3f0f7fe9c599" dependencies = [ "anchor-lang", "borsh 1.5.7", @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-commit" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975c1979c2f23b729cc0e6122f9ffc93daad4cb211502c2eac3297b5a8874fb" +checksum = "661e47aee1ed4137160107f956a65fb33db0c5073f52ee1231756e280accdd99" dependencies = [ "quote", "syn 1.0.109", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4905662d18fb332f1673419ad548a4c021c9a38270e9df768a16b34b055aee25" +checksum = "5a2879070124e9b50d018d1e4135d86e34e13faab4a597bab0c3266f05fb0fa2" dependencies = [ "proc-macro2", "quote", @@ -568,9 +568,9 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7cb320b197c7efae7c65965bb4c025d5bd08d58cdc3523a7d61ac2266ba3f4" +checksum = "f068f197b17e61812fe9cefc27d0d87ca22e3f17d328520189a7ca40531f34f4" dependencies = [ "proc-macro2", "quote", diff --git a/tests/integration/package-lock.json b/tests/integration/package-lock.json index 29c59778..96c462a6 100644 --- a/tests/integration/package-lock.json +++ b/tests/integration/package-lock.json @@ -8,7 +8,7 @@ "@coral-xyz/anchor": "0.31.1" }, "devDependencies": { - "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.10", + "@magicblock-labs/ephemeral-rollups-sdk": "0.2.11", "@metaplex-foundation/beet": "^0.7.1", "@metaplex-foundation/beet-solana": "^0.4.0", "@types/bn.js": "^5.1.0", @@ -21,6 +21,9 @@ "typescript": "^4.3.5" } }, + "../../../ts": { + "extraneous": true + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", diff --git a/tests/integration/package.json b/tests/integration/package.json index 28e94aef..ca002506 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -7,7 +7,7 @@ "@coral-xyz/anchor": "0.31.1" }, "devDependencies": { - "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.10", + "@magicblock-labs/ephemeral-rollups-sdk": "^0.2.11", "@metaplex-foundation/beet": "^0.7.1", "@metaplex-foundation/beet-solana": "^0.4.0", "@types/bn.js": "^5.1.0", diff --git a/tests/integration/programs/test-delegation/Cargo.toml b/tests/integration/programs/test-delegation/Cargo.toml index 6405e05e..ad93c701 100644 --- a/tests/integration/programs/test-delegation/Cargo.toml +++ b/tests/integration/programs/test-delegation/Cargo.toml @@ -18,4 +18,4 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = "0.31.1" -ephemeral-rollups-sdk = { version = "0.2.10", features = ["anchor"] } +ephemeral-rollups-sdk = { version = "0.2.12", features = ["anchor"] } diff --git a/tests/integration/programs/test-delegation/src/lib.rs b/tests/integration/programs/test-delegation/src/lib.rs index 979388c2..d9621f10 100644 --- a/tests/integration/programs/test-delegation/src/lib.rs +++ b/tests/integration/programs/test-delegation/src/lib.rs @@ -1,7 +1,6 @@ use anchor_lang::prelude::*; use ephemeral_rollups_sdk::anchor::{delegate, ephemeral}; use ephemeral_rollups_sdk::cpi::DelegateConfig; -use ephemeral_rollups_sdk::pda::ephemeral_balance_pda_from_payer; declare_id!("3vAK9JQiDsKoQNwmcfeEng4Cnv22pYuj1ASfso7U4ukF"); @@ -77,59 +76,42 @@ pub mod test_delegation { } /// Delegation program call handler - pub fn delegation_program_call_handler( - ctx: Context, - hook_args: delegation_program_utils::CallHandlerArgs, + #[instruction(discriminator = [1, 0, 1, 0])] + pub fn commit_base_action_handler( + ctx: Context, + amount: u64, ) -> Result<()> { - let expected = ephemeral_balance_pda_from_payer( - ctx.accounts.escrow_authority.key, - hook_args.escrow_index, + msg!("commit_base_action_handler!"); + let transfer_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow_account.to_account_info(), + to: ctx.accounts.destination_account.to_account_info(), + }, + ); + + transfer(transfer_ctx, amount) + } + + #[instruction(discriminator = [1, 0, 2, 0])] + pub fn undelegate_base_action_handler( + ctx: Context, + amount: u64, + ) -> Result<()> { + msg!("undelegate_base_action_handler"); + let transfer_ctx = CpiContext::new( + ctx.accounts.system_program.to_account_info(), + Transfer { + from: ctx.accounts.escrow_account.to_account_info(), + to: ctx.accounts.destination_account.to_account_info(), + }, ); - if &expected != ctx.accounts.escrow_account.key { - Err(ProgramError::InvalidAccountData) - } else { - Ok(()) - }?; - - if !ctx.accounts.escrow_account.is_signer { - Err(ProgramError::MissingRequiredSignature) - } else { - Ok(()) - }?; - - match hook_args.context { - delegation_program_utils::Context::Commit => { - msg!("commit context"); - let amount = u64::try_from_slice(&hook_args.data)?; - let transfer_ctx = CpiContext::new( - ctx.accounts.system_program.to_account_info(), - Transfer { - from: ctx.accounts.escrow_account.to_account_info(), - to: ctx.accounts.destination_account.to_account_info(), - }, - ); - transfer(transfer_ctx, amount)?; - } - delegation_program_utils::Context::Undelegate => { - msg!("undelegate context"); - let amount = u64::try_from_slice(&hook_args.data)?; - let transfer_ctx = CpiContext::new( - ctx.accounts.system_program.to_account_info(), - Transfer { - from: ctx.accounts.escrow_account.to_account_info(), - to: ctx.accounts.destination_account.to_account_info(), - }, - ); - transfer(transfer_ctx, amount)?; - - let counter_data = &mut ctx.accounts.counter.try_borrow_mut_data()?; - let mut counter = Counter::try_from_slice(&counter_data)?; - counter.count += 1; - - counter_data.copy_from_slice(&counter.try_to_vec()?); - } - delegation_program_utils::Context::Standalone => msg!("standalone context"), - } + transfer(transfer_ctx, amount)?; + + let counter_data = &mut ctx.accounts.counter.try_borrow_mut_data()?; + let mut counter = Counter::try_from_slice(&counter_data)?; + counter.count += 1; + counter_data.copy_from_slice(&counter.try_to_vec()?); Ok(()) } @@ -207,23 +189,27 @@ pub struct Increment<'info> { } #[derive(Accounts)] -#[instruction(hook_args: delegation_program_utils::CallHandlerArgs)] -pub struct DelegationProgramCallHandler<'info> { +pub struct CommitBaseActionHandler<'info> { + /// CHECK: The destination account to transfer lamports to + #[account(mut)] + pub destination_account: AccountInfo<'info>, + pub system_program: Program<'info, System>, /// CHECK: The authority that owns the escrow account pub escrow_authority: UncheckedAccount<'info>, - #[account( - mut, - seeds = [b"balance", &escrow_authority.key().as_ref(), &[hook_args.escrow_index]], - seeds::program = delegation_program_utils::ID, - bump - )] pub escrow_account: Signer<'info>, +} + +#[derive(Accounts)] +pub struct UndelegateBaseActionHandler<'info> { /// CHECK: The destination account to transfer lamports to #[account(mut)] pub destination_account: AccountInfo<'info>, /// CHECK: fails in finalize stage due to ownership by dlp pub counter: UncheckedAccount<'info>, pub system_program: Program<'info, System>, + /// CHECK: The authority that owns the escrow account + pub escrow_authority: UncheckedAccount<'info>, + pub escrow_account: Signer<'info>, } #[account] @@ -235,18 +221,4 @@ mod delegation_program_utils { use anchor_lang::prelude::*; declare_id!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh"); - - #[derive(AnchorSerialize, AnchorDeserialize)] - pub enum Context { - Commit, - Undelegate, - Standalone, - } - - #[derive(AnchorSerialize, AnchorDeserialize)] - pub struct CallHandlerArgs { - pub escrow_index: u8, - pub data: Vec, - pub context: Context, - } } diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index e23d661d..0a988132 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -10,6 +10,7 @@ import { } from "@magicblock-labs/ephemeral-rollups-sdk"; import { ON_CURVE_ACCOUNT } from "./fixtures/consts"; import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { assert } from "chai"; const SEED_TEST_PDA = "test-pda"; const BPF_LOADER = new web3.PublicKey( @@ -34,6 +35,18 @@ describe("TestDelegation", () => { const ownerProgram = testDelegation.programId; const reimbursement = provider.wallet.publicKey; + async function fetchTransaction(txhash: string) { + for (let i = 0; i < 10; i++) { + const tx = await provider.connection.getTransaction(txhash, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }); + if (tx) return tx; + await new Promise((r) => setTimeout(r, 500)); // wait 0.5s + } + throw new Error("Transaction not found after waiting"); + } + it("Initialize protocol fees vault", async () => { const ix = createInitFeesVaultInstruction(payer); const txId = await processInstruction(ix); @@ -96,13 +109,57 @@ describe("TestDelegation", () => { console.log("Counter: ", counterAccount.count.toString()); }); + // .skip() because currently tests are not independent and we cannot run two similar tests twice or more. + it.skip("Delegate one PDA", async () => { + const counterAccountInfo = await provider.connection.getAccountInfo(pda); + if (counterAccountInfo === null) { + const tx = await testDelegation.methods + .initialize() + .accounts({ + user: provider.wallet.publicKey, + }) + .rpc({ skipPreflight: true }); + console.log("Init Pda Tx: ", tx); + } + + const ca = await provider.connection.getAccountInfo(pda); + console.log(`owner: ${ca?.owner?.toBase58()}`); + + // Delegate 1 PDA in a single instruction + const txhash = await testDelegation.methods + .delegate() + .accounts({ + payer: provider.wallet.publicKey, + }) + .rpc({ skipPreflight: true }); + console.log("Your transaction signature", txhash); + + const tx = await fetchTransaction(txhash); + console.log(tx.meta.logMessages); + + const consumedLog = tx.meta.logMessages.find((m) => + m.includes("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh consumed") + ); + + assert.isAtMost( + parseInt(consumedLog.split(" ").at(3)), + 18500, + "delegate instruction must consume less than 18500" + ); + + const counterAccount = await provider.connection.getAccountInfo(pda); + assert.strictEqual( + counterAccount.owner.toBase58(), + "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh" + ); + }); + it("Delegate two PDAs", async () => { // Delegate 2 PDAs in a single instruction const tx = await testDelegation.methods .delegateTwo() .accounts({ payer: provider.wallet.publicKey, - validator, }) .rpc({ skipPreflight: true }); console.log("Your transaction signature", tx); @@ -164,12 +221,37 @@ describe("TestDelegation", () => { ); const txId = await processInstruction(ix); console.log("Commit state signature", txId); + + const tx = await fetchTransaction(txId); + console.log(tx.meta.logMessages); + + const consumedLog = tx.meta.logMessages.find((m) => + m.includes("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh consumed") + ); + + assert.isAtMost( + parseInt(consumedLog.split(" ").at(3)), + 32000, + "commit instruction must consume less than 32000" + ); }); it("Finalize account state", async () => { const ix = createFinalizeInstruction(validator, pda); const txId = await processInstruction(ix); console.log("Finalize signature", txId); + const tx = await fetchTransaction(txId); + console.log(tx.meta.logMessages); + + const consumedLog = tx.meta.logMessages.find((m) => + m.includes("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh consumed") + ); + + assert.isAtMost( + parseInt(consumedLog.split(" ").at(3)), + 17500, + "finalize instruction must consume less than 17500" + ); }); it("Commit a new state to the PDA", async () => { @@ -208,6 +290,19 @@ describe("TestDelegation", () => { ); const txId = await processInstruction(ix); console.log("Undelegate signature", txId); + + const tx = await fetchTransaction(txId); + console.log(tx.meta.logMessages); + + const consumedLog = tx.meta.logMessages.find((m) => + m.includes("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh consumed") + ); + + assert.isAtMost( + parseInt(consumedLog.split(" ").at(3)), + 45000, + "undelegate instruction must consume less than 18500" + ); }); it("Whitelist a validator for a program", async () => { diff --git a/tests/integration/yarn.lock b/tests/integration/yarn.lock deleted file mode 100644 index e8db6d4e..00000000 --- a/tests/integration/yarn.lock +++ /dev/null @@ -1,1445 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/runtime@^7.25.0": - version "7.28.4" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz" - integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== - -"@coral-xyz/anchor-errors@^0.31.1": - version "0.31.1" - resolved "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz" - integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== - -"@coral-xyz/anchor@0.31.1": - version "0.31.1" - resolved "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.31.1.tgz" - integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== - dependencies: - "@coral-xyz/anchor-errors" "^0.31.1" - "@coral-xyz/borsh" "^0.31.1" - "@noble/hashes" "^1.3.1" - "@solana/web3.js" "^1.69.0" - bn.js "^5.1.2" - bs58 "^4.0.1" - buffer-layout "^1.2.2" - camelcase "^6.3.0" - cross-fetch "^3.1.5" - eventemitter3 "^4.0.7" - pako "^2.0.3" - superstruct "^0.15.4" - toml "^3.0.0" - -"@coral-xyz/borsh@^0.31.1": - version "0.31.1" - resolved "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.31.1.tgz" - integrity sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw== - dependencies: - bn.js "^5.1.2" - buffer-layout "^1.2.0" - -"@magicblock-labs/ephemeral-rollups-sdk@^0.2.10": - version "0.2.11" - resolved "https://registry.npmjs.org/@magicblock-labs/ephemeral-rollups-sdk/-/ephemeral-rollups-sdk-0.2.11.tgz" - integrity sha512-I9pqYBsWhkUpndt9b4P6jqpVhAKaMdyyiBuxEX+Y2VdWeXuRpF0qXngyn26i1oKCVvbPNQYLu033LpNeSB4BKg== - dependencies: - "@metaplex-foundation/beet" "^0.7.2" - "@phala/dcap-qvl-web" "^0.2.7" - "@solana/web3.js" "^1.98.0" - bs58 "^6.0.0" - rpc-websockets "^9.0.4" - typescript "^5.3.0" - -"@metaplex-foundation/beet-solana@^0.4.0": - version "0.4.1" - resolved "https://registry.npmjs.org/@metaplex-foundation/beet-solana/-/beet-solana-0.4.1.tgz" - integrity sha512-/6o32FNUtwK8tjhotrvU/vorP7umBuRFvBZrC6XCk51aKidBHe5LPVPA5AjGPbV3oftMfRuXPNd9yAGeEqeCDQ== - dependencies: - "@metaplex-foundation/beet" ">=0.1.0" - "@solana/web3.js" "^1.56.2" - bs58 "^5.0.0" - debug "^4.3.4" - -"@metaplex-foundation/beet@^0.7.1", "@metaplex-foundation/beet@^0.7.2", "@metaplex-foundation/beet@>=0.1.0": - version "0.7.2" - resolved "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.2.tgz" - integrity sha512-K+g3WhyFxKPc0xIvcIjNyV1eaTVJTiuaHZpig7Xx0MuYRMoJLLvhLTnUXhFdR5Tu2l2QSyKwfyXDgZlzhULqFg== - dependencies: - ansicolors "^0.3.2" - assert "^2.1.0" - bn.js "^5.2.0" - debug "^4.3.3" - -"@noble/curves@^1.4.2": - version "1.7.0" - resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz" - integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== - dependencies: - "@noble/hashes" "1.6.0" - -"@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== - -"@noble/hashes@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz" - integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== - -"@phala/dcap-qvl-web@^0.2.7": - version "0.2.7" - resolved "https://registry.npmjs.org/@phala/dcap-qvl-web/-/dcap-qvl-web-0.2.7.tgz" - integrity sha512-OgDIN8ZRsLg0dJgUAk0HCXMjkAmrif7p0C+P74YrtxgE/8fNSFpqNDjVW3mCVB2Q/V7X6mUhbEQWa5wJmM9OSQ== - -"@solana/buffer-layout@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz" - integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== - dependencies: - buffer "~6.0.3" - -"@solana/codecs-core@2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.1.0.tgz" - integrity sha512-SR7pKtmJBg2mhmkel2NeHA1pz06QeQXdMv8WJoIR9m8F/hw80K/612uaYbwTt2nkK0jg/Qn/rNSd7EcJ4SBGjw== - dependencies: - "@solana/errors" "2.1.0" - -"@solana/codecs-numbers@^2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.1.0.tgz" - integrity sha512-XMu4yw5iCgQnMKsxSWPPOrGgtaohmupN3eyAtYv3K3C/MJEc5V90h74k5B1GUCiHvcrdUDO9RclNjD9lgbjFag== - dependencies: - "@solana/codecs-core" "2.1.0" - "@solana/errors" "2.1.0" - -"@solana/errors@2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@solana/errors/-/errors-2.1.0.tgz" - integrity sha512-l+GxAv0Ar4d3c3PlZdA9G++wFYZREEbbRyAFP8+n8HSg0vudCuzogh/13io6hYuUhG/9Ve8ARZNamhV7UScKNw== - dependencies: - chalk "^5.3.0" - commander "^13.1.0" - -"@solana/web3.js@^1.56.2", "@solana/web3.js@^1.69.0", "@solana/web3.js@^1.98.0": - version "1.98.4" - resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz" - integrity sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw== - dependencies: - "@babel/runtime" "^7.25.0" - "@noble/curves" "^1.4.2" - "@noble/hashes" "^1.4.0" - "@solana/buffer-layout" "^4.0.1" - "@solana/codecs-numbers" "^2.1.0" - agentkeepalive "^4.5.0" - bn.js "^5.2.1" - borsh "^0.7.0" - bs58 "^4.0.1" - buffer "6.0.3" - fast-stable-stringify "^1.0.0" - jayson "^4.1.1" - node-fetch "^2.7.0" - rpc-websockets "^9.0.2" - superstruct "^2.0.2" - -"@swc/helpers@^0.5.11": - version "0.5.15" - resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== - dependencies: - tslib "^2.8.0" - -"@types/bn.js@^5.1.0": - version "5.1.5" - resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz" - integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== - dependencies: - "@types/node" "*" - -"@types/chai@^4.3.0": - version "4.3.14" - resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz" - integrity sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w== - -"@types/connect@^3.4.33": - version "3.4.38" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/mocha@^9.0.0": - version "9.1.1" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz" - integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== - -"@types/node@*": - version "20.12.7" - resolved "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz" - integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== - dependencies: - undici-types "~5.26.4" - -"@types/node@^12.12.54": - version "12.20.55" - resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== - -"@types/uuid@^8.3.4": - version "8.3.4" - resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz" - integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== - -"@types/ws@^7.4.4": - version "7.4.7" - resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== - dependencies: - "@types/node" "*" - -"@types/ws@^8.2.2": - version "8.5.13" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz" - integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== - dependencies: - "@types/node" "*" - -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - -agentkeepalive@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" - integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== - dependencies: - humanize-ms "^1.2.1" - -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansicolors@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz" - integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - -assert@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base-x@^3.0.2: - version "3.0.11" - resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz" - integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== - dependencies: - safe-buffer "^5.0.1" - -base-x@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz" - integrity sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw== - -base-x@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz" - integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -borsh@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz" - integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== - dependencies: - bn.js "^5.2.0" - bs58 "^4.0.0" - text-encoding-utf-8 "^1.0.2" - -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -bs58@^4.0.0, bs58@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz" - integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== - dependencies: - base-x "^3.0.2" - -bs58@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz" - integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== - dependencies: - base-x "^4.0.0" - -bs58@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz" - integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== - dependencies: - base-x "^5.0.0" - -buffer-from@^1.0.0, buffer-from@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-layout@^1.2.0, buffer-layout@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz" - integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== - -buffer@^6.0.3, buffer@~6.0.3, buffer@6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -bufferutil@^4.0.1: - version "4.0.8" - resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz" - integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== - dependencies: - node-gyp-build "^4.3.0" - -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - -camelcase@^6.0.0, camelcase@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -chai@^4.3.4: - version "4.4.1" - resolved "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz" - integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.0.8" - -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.3.0: - version "5.4.1" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" - -chokidar@3.5.3: - version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -commander@^13.1.0: - version "13.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz" - integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== - -commander@^2.20.3: - version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -cross-fetch@^3.1.5: - version "3.1.8" - resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - -debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debug@4.3.3: - version "4.3.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - -deep-eql@^4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz" - integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== - dependencies: - type-detect "^4.0.0" - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - -diff@^3.1.0: - version "3.5.0" - resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -diff@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" - integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== - dependencies: - es6-promise "^4.0.3" - -escalade@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escape-string-regexp@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eventemitter3@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -eyes@^0.1.8: - version "0.1.8" - resolved "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz" - integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== - -fast-stable-stringify@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz" - integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-func-name@^2.0.1, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@^2.0.3, inherits@2: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - -jayson@^4.1.1: - version "4.1.3" - resolved "https://registry.npmjs.org/jayson/-/jayson-4.1.3.tgz" - integrity sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ== - dependencies: - "@types/connect" "^3.4.33" - "@types/node" "^12.12.54" - "@types/ws" "^7.4.4" - commander "^2.20.3" - delay "^5.0.0" - es6-promisify "^5.0.0" - eyes "^0.1.8" - isomorphic-ws "^4.0.1" - json-stringify-safe "^5.0.1" - JSONStream "^1.3.5" - uuid "^8.3.2" - ws "^7.5.10" - -js-yaml@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-stringify-safe@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -log-symbols@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -loupe@^2.3.6: - version "2.3.7" - resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== - dependencies: - get-func-name "^2.0.1" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mkdirp@^0.5.1: - version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -"mocha@^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X", mocha@^9.0.3: - version "9.2.2" - resolved "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz" - integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.3" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.2.0" - growl "1.10.5" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "4.2.1" - ms "2.1.3" - nanoid "3.3.1" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - -ms@^2.0.0, ms@2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nanoid@3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== - -node-fetch@^2.6.12, node-fetch@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-gyp-build@^4.3.0: - version "4.8.0" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz" - integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4: - version "4.1.5" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -pako@^2.0.3: - version "2.1.0" - resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" - integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -prettier@^2.6.2: - version "2.8.8" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -rpc-websockets@^9.0.2, rpc-websockets@^9.0.4: - version "9.0.4" - resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.4.tgz" - integrity sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ== - dependencies: - "@swc/helpers" "^0.5.11" - "@types/uuid" "^8.3.4" - "@types/ws" "^8.2.2" - buffer "^6.0.3" - eventemitter3 "^5.0.1" - uuid "^8.3.2" - ws "^8.5.0" - optionalDependencies: - bufferutil "^4.0.1" - utf-8-validate "^5.0.2" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -source-map-support@^0.5.6: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-json-comments@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -superstruct@^0.15.4: - version "0.15.5" - resolved "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz" - integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== - -superstruct@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz" - integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -text-encoding-utf-8@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - -"through@>=2.2.7 <3": - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -ts-mocha@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz" - integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== - dependencies: - ts-node "7.0.1" - optionalDependencies: - tsconfig-paths "^3.5.0" - -ts-node@7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz" - integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== - dependencies: - arrify "^1.0.0" - buffer-from "^1.1.0" - diff "^3.1.0" - make-error "^1.1.1" - minimist "^1.2.0" - mkdirp "^0.5.1" - source-map-support "^0.5.6" - yn "^2.0.0" - -tsconfig-paths@^3.5.0: - version "3.15.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^2.8.0: - version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -type-detect@^4.0.0, type-detect@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -typescript@^4.3.5, typescript@>=5: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - -typescript@^5.3.0: - version "5.9.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -utf-8-validate@^5.0.2, utf-8-validate@>=5.0.2: - version "5.0.10" - resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" - integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== - dependencies: - node-gyp-build "^4.3.0" - -util@^0.12.5: - version "0.12.5" - resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which@2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@*, ws@^7.5.10: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^8.5.0: - version "8.18.3" - resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" - integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yargs-parser@^20.2.2, yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-unparser@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yn@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz" - integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/tests/test_call_handler.rs b/tests/test_call_handler.rs index 5b92652e..0d2913a4 100644 --- a/tests/test_call_handler.rs +++ b/tests/test_call_handler.rs @@ -14,7 +14,7 @@ use dlp::pda::{ use solana_program::instruction::AccountMeta; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::pubkey::Pubkey; use solana_sdk::{ account::Account, @@ -24,6 +24,9 @@ use solana_sdk::{ mod fixtures; +const COMMIT_HANDLER_DISCRIMINATOR: [u8; 4] = [1, 0, 1, 0]; +const UNDELEGATE_HANDLER_DISCRIMINATOR: [u8; 4] = [1, 0, 2, 0]; + // Mimic counter from test_delegation program #[derive(BorshSerialize, BorshDeserialize)] pub struct Counter { @@ -216,7 +219,7 @@ async fn setup_ephemeral_balance(program_test: &mut ProgramTest, payer: &Keypair } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let payer = Keypair::new(); @@ -296,13 +299,15 @@ async fn test_finalize_call_handler() { payer.pubkey(), // escrow authority vec![ AccountMeta::new(transfer_destination.pubkey(), false), - AccountMeta::new(DELEGATED_PDA_ID, false), AccountMeta::new_readonly(system_program::id(), false), ], CallHandlerArgs { escrow_index: 2, // undelegated escrow index, - data: to_vec(&PRIZE).unwrap(), - context: dlp::args::Context::Commit, + data: [ + COMMIT_HANDLER_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), }, ); @@ -350,8 +355,11 @@ async fn test_undelegate_call_handler() { ], CallHandlerArgs { escrow_index: 2, // undelegated escrow index, - data: to_vec(&PRIZE).unwrap(), - context: dlp::args::Context::Undelegate, + data: [ + UNDELEGATE_HANDLER_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), }, ); @@ -399,8 +407,7 @@ async fn test_finalize_invalid_escrow_call_handler() { vec![AccountMeta::new(transfer_destination.pubkey(), false)], CallHandlerArgs { escrow_index: 0, - data: vec![], - context: dlp::args::Context::Commit, + data: COMMIT_HANDLER_DISCRIMINATOR.to_vec(), }, ); let tx = Transaction::new_signed_with_payer( @@ -432,8 +439,7 @@ async fn test_undelegate_invalid_escow_call_handler() { vec![AccountMeta::new(destination.pubkey(), false)], CallHandlerArgs { escrow_index: 0, - data: vec![], - context: dlp::args::Context::Commit, + data: UNDELEGATE_HANDLER_DISCRIMINATOR.to_vec(), }, ); @@ -450,8 +456,11 @@ async fn test_undelegate_invalid_escow_call_handler() { vec![AccountMeta::new(destination.pubkey(), false)], CallHandlerArgs { escrow_index: 0, - data: to_vec(&PRIZE).unwrap(), - context: dlp::args::Context::Undelegate, + data: [ + UNDELEGATE_HANDLER_DISCRIMINATOR.to_vec(), + to_vec(&PRIZE).unwrap(), + ] + .concat(), }, ); let tx = Transaction::new_signed_with_payer( diff --git a/tests/test_close_validator_fees_vault.rs b/tests/test_close_validator_fees_vault.rs index 719fadca..9bbcb8f4 100644 --- a/tests/test_close_validator_fees_vault.rs +++ b/tests/test_close_validator_fees_vault.rs @@ -1,7 +1,7 @@ use crate::fixtures::TEST_AUTHORITY; use dlp::pda::validator_fees_vault_pda_from_validator; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -33,7 +33,7 @@ async fn test_close_validator_fees_vault() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_commit_on_curve.rs b/tests/test_commit_on_curve.rs index b66f685d..e86bf9cb 100644 --- a/tests/test_commit_on_curve.rs +++ b/tests/test_commit_on_curve.rs @@ -7,7 +7,7 @@ use dlp::pda::{ use dlp::state::{CommitRecord, DelegationMetadata}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -79,7 +79,7 @@ async fn test_commit_on_curve() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); // Setup the validator authority diff --git a/tests/test_commit_state.rs b/tests/test_commit_state.rs index 50e9d418..07c32f99 100644 --- a/tests/test_commit_state.rs +++ b/tests/test_commit_state.rs @@ -7,7 +7,7 @@ use dlp::pda::{ use dlp::state::{CommitRecord, DelegationMetadata}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -120,7 +120,7 @@ async fn test_commit_out_of_order() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_commit_state_from_buffer.rs b/tests/test_commit_state_from_buffer.rs index d2047dd8..cac16ac4 100644 --- a/tests/test_commit_state_from_buffer.rs +++ b/tests/test_commit_state_from_buffer.rs @@ -11,7 +11,7 @@ use dlp::pda::{ use dlp::state::{CommitRecord, DelegationMetadata}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::pubkey::Pubkey; use solana_sdk::{ account::Account, @@ -85,7 +85,7 @@ async fn test_commit_new_state_from_buffer() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_commit_state_with_program_config.rs b/tests/test_commit_state_with_program_config.rs index 2d9716a6..048f7164 100644 --- a/tests/test_commit_state_with_program_config.rs +++ b/tests/test_commit_state_with_program_config.rs @@ -8,7 +8,7 @@ use dlp::state::{CommitRecord, DelegationMetadata}; use fixtures::create_program_config_data; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -99,7 +99,7 @@ async fn test_commit_new_state(valid_config: bool) { } async fn setup_program_test_env(valid_config: bool) -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_delegate.rs b/tests/test_delegate.rs index c706e5a3..37173765 100644 --- a/tests/test_delegate.rs +++ b/tests/test_delegate.rs @@ -1,7 +1,7 @@ use solana_program::pubkey::Pubkey; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::{ account::Account, @@ -77,7 +77,8 @@ async fn test_delegate() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); let payer_alt = Keypair::new(); diff --git a/tests/test_delegate_on_curve.rs b/tests/test_delegate_on_curve.rs index 8628442a..1ded45ee 100644 --- a/tests/test_delegate_on_curve.rs +++ b/tests/test_delegate_on_curve.rs @@ -1,5 +1,5 @@ use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -121,7 +121,7 @@ async fn test_delegate_on_curve() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let payer_alt = Keypair::from_bytes(&ON_CURVE_KEYPAIR).unwrap(); diff --git a/tests/test_finalize.rs b/tests/test_finalize.rs index fcfe5a31..08a05552 100644 --- a/tests/test_finalize.rs +++ b/tests/test_finalize.rs @@ -10,7 +10,7 @@ use dlp::pda::{ use dlp::state::{CommitRecord, DelegationMetadata}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -83,7 +83,7 @@ async fn test_finalize() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let authority = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_init_fees_vault.rs b/tests/test_init_fees_vault.rs index cf4db900..3b59457b 100644 --- a/tests/test_init_fees_vault.rs +++ b/tests/test_init_fees_vault.rs @@ -1,6 +1,6 @@ use dlp::pda::fees_vault_pda; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -26,7 +26,7 @@ async fn test_init_fees_vault() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let payer_alt = Keypair::new(); diff --git a/tests/test_init_validator_fees_vault.rs b/tests/test_init_validator_fees_vault.rs index f95772cb..1e3eae97 100644 --- a/tests/test_init_validator_fees_vault.rs +++ b/tests/test_init_validator_fees_vault.rs @@ -2,7 +2,7 @@ use crate::fixtures::TEST_AUTHORITY; use dlp::pda::validator_fees_vault_pda_from_validator; use solana_program::pubkey::Pubkey; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -49,7 +49,7 @@ async fn test_init_validator_fees_vault() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_lamports_settlement.rs b/tests/test_lamports_settlement.rs index 46ec383f..431d89b1 100644 --- a/tests/test_lamports_settlement.rs +++ b/tests/test_lamports_settlement.rs @@ -13,7 +13,7 @@ use dlp::state::{CommitRecord, DelegationMetadata}; use solana_program::pubkey::Pubkey; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -531,7 +531,7 @@ struct SetupProgramCommitTestEnvArgs { async fn setup_program_for_commit_test_env( args: SetupProgramCommitTestEnvArgs, ) -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 1c2c0b91..c7d3ace0 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -2,10 +2,10 @@ use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; use dlp::pda::{fees_vault_pda, program_config_from_program_id}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; -use solana_sdk::pubkey::Pubkey; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, + pubkey::Pubkey, signature::{Keypair, Signer}, transaction::Transaction, }; @@ -42,7 +42,7 @@ async fn test_protocol_claim_fees() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_top_up.rs b/tests/test_top_up.rs index e55daf20..8df8af73 100644 --- a/tests/test_top_up.rs +++ b/tests/test_top_up.rs @@ -10,7 +10,7 @@ use dlp::pda::{ use dlp::state::DelegationRecord; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::{Account, ReadableAccount}, signature::{Keypair, Signer}, @@ -261,7 +261,7 @@ async fn test_undelegate_and_close() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let payer_alt = Keypair::new(); diff --git a/tests/test_undelegate.rs b/tests/test_undelegate.rs index 9135c280..684d3e13 100644 --- a/tests/test_undelegate.rs +++ b/tests/test_undelegate.rs @@ -5,7 +5,7 @@ use dlp::pda::{ }; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -76,7 +76,7 @@ async fn test_finalize_and_undelegate() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let authority = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_undelegate_on_curve.rs b/tests/test_undelegate_on_curve.rs index 8e2f7cb2..307a4539 100644 --- a/tests/test_undelegate_on_curve.rs +++ b/tests/test_undelegate_on_curve.rs @@ -4,7 +4,7 @@ use dlp::pda::{ }; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -64,7 +64,7 @@ async fn test_undelegate_on_curve() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); let payer_alt = Keypair::from_bytes(&ON_CURVE_KEYPAIR).unwrap(); diff --git a/tests/test_undelegate_without_commit.rs b/tests/test_undelegate_without_commit.rs index f0ce4fe3..6c1e31f5 100644 --- a/tests/test_undelegate_without_commit.rs +++ b/tests/test_undelegate_without_commit.rs @@ -5,7 +5,7 @@ use dlp::pda::{ }; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -76,7 +76,7 @@ async fn test_undelegate_without_commit() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_validator_claim_fees.rs b/tests/test_validator_claim_fees.rs index 4a5db94b..8c989021 100644 --- a/tests/test_validator_claim_fees.rs +++ b/tests/test_validator_claim_fees.rs @@ -2,7 +2,7 @@ use crate::fixtures::TEST_AUTHORITY; use dlp::consts::PROTOCOL_FEES_PERCENTAGE; use dlp::pda::{fees_vault_pda, validator_fees_vault_pda_from_validator}; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -78,7 +78,7 @@ async fn test_validator_claim_fees() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); diff --git a/tests/test_whitelist_validator_for_program.rs b/tests/test_whitelist_validator_for_program.rs index d5a7788b..bc6d8d4a 100644 --- a/tests/test_whitelist_validator_for_program.rs +++ b/tests/test_whitelist_validator_for_program.rs @@ -3,7 +3,7 @@ use dlp::pda::program_config_from_program_id; use dlp::state::ProgramConfig; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, read_file, BanksClient, ProgramTest}; +use solana_program_test::{read_file, BanksClient, ProgramTest}; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -98,7 +98,7 @@ async fn test_remove_validator_for_program() { } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let validator = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); From e7cf69c4a86e98be8d0f4cfcb404c274e260d5e8 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 28 Oct 2025 19:31:32 +0100 Subject: [PATCH 05/29] fix: move fees receiver to fees vault PDA --- .../init_protocol_fees_vault.rs | 12 +++-- .../protocol_claim_fees.rs | 4 +- src/instruction_builder/set_fees_receiver.rs | 11 ++-- src/lib.rs | 6 +-- src/processor/close_validator_fees_vault.rs | 5 +- src/processor/init_protocol_fees_vault.rs | 37 +++++++++++--- src/processor/protocol_claim_fees.rs | 30 +++++------ src/processor/set_fees_receiver.rs | 48 ++++++++---------- src/state/fees_vault.rs | 26 ++++++++++ src/state/mod.rs | 2 + src/state/utils/discriminator.rs | 1 + .../programs/test-delegation/Cargo.toml | 3 ++ tests/integration/tests/test-delegation.ts | 15 +++--- tests/test_protocol_claim_fees.rs | 17 ++++--- tests/test_set_fees_receiver.rs | 50 +++++++++++++------ 15 files changed, 171 insertions(+), 96 deletions(-) create mode 100644 src/state/fees_vault.rs diff --git a/src/instruction_builder/init_protocol_fees_vault.rs b/src/instruction_builder/init_protocol_fees_vault.rs index 7192a877..4b2517c7 100644 --- a/src/instruction_builder/init_protocol_fees_vault.rs +++ b/src/instruction_builder/init_protocol_fees_vault.rs @@ -1,6 +1,9 @@ -use solana_program::instruction::Instruction; -use solana_program::system_program; -use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; +use solana_program::{ + bpf_loader_upgradeable, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; use crate::discriminator::DlpDiscriminator; use crate::pda::fees_vault_pda; @@ -9,11 +12,14 @@ use crate::pda::fees_vault_pda; /// See [crate::processor::process_init_protocol_fees_vault] for docs. pub fn init_protocol_fees_vault(payer: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); + let delegation_program_data = + Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { program_id: crate::id(), accounts: vec![ AccountMeta::new(payer, true), AccountMeta::new(fees_vault_pda, false), + AccountMeta::new_readonly(delegation_program_data, false), AccountMeta::new_readonly(system_program::id(), false), ], data: DlpDiscriminator::InitProtocolFeesVault.to_vec(), diff --git a/src/instruction_builder/protocol_claim_fees.rs b/src/instruction_builder/protocol_claim_fees.rs index 4a5b424e..01784e70 100644 --- a/src/instruction_builder/protocol_claim_fees.rs +++ b/src/instruction_builder/protocol_claim_fees.rs @@ -2,18 +2,16 @@ use solana_program::instruction::Instruction; use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; use crate::discriminator::DlpDiscriminator; -use crate::pda::{fees_vault_pda, program_config_from_program_id}; +use crate::pda::fees_vault_pda; /// Claim the accrued fees from the protocol fees vault. /// See [crate::processor::process_protocol_claim_fees] for docs. pub fn protocol_claim_fees(fees_receiver: Pubkey, program: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); - let program_config_pda = program_config_from_program_id(&program); Instruction { program_id: crate::id(), accounts: vec![ AccountMeta::new(fees_vault_pda, false), - AccountMeta::new(program_config_pda, false), AccountMeta::new(fees_receiver, false), AccountMeta::new_readonly(program, false), ], diff --git a/src/instruction_builder/set_fees_receiver.rs b/src/instruction_builder/set_fees_receiver.rs index 439a893b..e9bbcb4b 100644 --- a/src/instruction_builder/set_fees_receiver.rs +++ b/src/instruction_builder/set_fees_receiver.rs @@ -6,22 +6,21 @@ use solana_program::{ use crate::args::SetFeesReceiverArgs; use crate::discriminator::DlpDiscriminator; -use crate::pda::program_config_from_program_id; +use crate::pda::fees_vault_pda; /// Set the fees receiver. /// See [crate::processor::process_set_fees_receiver] for docs. -pub fn set_fees_receiver(admin: Pubkey, fees_receiver: Pubkey, program: Pubkey) -> Instruction { - let program_config_pda = program_config_from_program_id(&program); +pub fn set_fees_receiver(admin: Pubkey, fees_receiver: Pubkey) -> Instruction { + let fees_vault_pda = fees_vault_pda(); let delegation_program_data = Pubkey::find_program_address(&[crate::ID.as_ref()], &bpf_loader_upgradeable::id()).0; Instruction { program_id: crate::id(), accounts: vec![ AccountMeta::new(admin, true), - AccountMeta::new(program_config_pda, false), - AccountMeta::new_readonly(program, false), - AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(fees_vault_pda, false), AccountMeta::new_readonly(delegation_program_data, false), + AccountMeta::new_readonly(system_program::id(), false), ], data: [ DlpDiscriminator::SetFeesReceiver.to_vec(), diff --git a/src/lib.rs b/src/lib.rs index cdcd6824..6b307aa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,13 +125,13 @@ pub fn slow_process_instruction( DlpDiscriminator::CallHandler => { processor::process_call_handler(program_id, accounts, data)? } + DlpDiscriminator::SetFeesReceiver => { + processor::process_set_fees_receiver(program_id, accounts, data)? + } _ => { log!("PANIC: Instruction must be processed by fast_process_instruction"); return Err(ProgramError::InvalidInstructionData); } - discriminator::DlpDiscriminator::SetFeesReceiver => { - processor::process_set_fees_receiver(program_id, accounts, data)? - } } Ok(()) } diff --git a/src/processor/close_validator_fees_vault.rs b/src/processor/close_validator_fees_vault.rs index fa838aca..bf1c56d7 100644 --- a/src/processor/close_validator_fees_vault.rs +++ b/src/processor/close_validator_fees_vault.rs @@ -15,8 +15,9 @@ use crate::validator_fees_vault_seeds_from_validator; /// /// 0; `[signer]` payer /// 1; `[signer]` admin that controls the vault -/// 2; `[]` validator_identity -/// 3; `[]` validator_fees_vault_pda +/// 2; `[]` delegation program +/// 3; `[]` validator_identity +/// 4; `[]` validator_fees_vault_pda /// /// Requirements: /// diff --git a/src/processor/init_protocol_fees_vault.rs b/src/processor/init_protocol_fees_vault.rs index 498c2c4f..e79835b8 100644 --- a/src/processor/init_protocol_fees_vault.rs +++ b/src/processor/init_protocol_fees_vault.rs @@ -1,18 +1,27 @@ -use solana_program::program_error::ProgramError; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey, system_program, + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, system_program, }; -use crate::fees_vault_seeds; -use crate::processor::utils::loaders::{load_program, load_signer, load_uninitialized_pda}; -use crate::processor::utils::pda::create_pda; +use crate::{ + error::DlpError::Unauthorized, + fees_vault_seeds, + processor::utils::{ + loaders::{ + load_program, load_program_upgrade_authority, load_signer, load_uninitialized_pda, + }, + pda::create_pda, + }, + state::FeesVault, +}; /// Initialize the global fees vault /// /// Accounts: /// 0: `[signer]` the account paying for the transaction /// 1: `[writable]` the fees vault PDA we are initializing -/// 2: `[]` the system program +/// 2: `[]` the delegation program data +/// 3: `[]` the system program /// /// Requirements: /// @@ -29,7 +38,7 @@ pub fn process_init_protocol_fees_vault( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [payer, protocol_fees_vault, system_program] = accounts else { + let [payer, protocol_fees_vault, delegation_program_data, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -44,16 +53,28 @@ pub fn process_init_protocol_fees_vault( "fees vault", )?; + // Check if the admin is the correct one + let admin_pubkey = + load_program_upgrade_authority(&crate::ID, delegation_program_data)?.ok_or(Unauthorized)?; + + let fees_vault = FeesVault { + fees_receiver: admin_pubkey, + }; + // Create the fees vault account create_pda( protocol_fees_vault, &crate::id(), - 8, + fees_vault.size_with_discriminator(), fees_vault_seeds!(), bump_fees_vault, system_program, payer, )?; + // Write the fees vault data + let mut fees_vault_data = protocol_fees_vault.try_borrow_mut_data()?; + fees_vault.to_bytes_with_discriminator(&mut fees_vault_data.as_mut())?; + Ok(()) } diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 48ccd946..a0edd29c 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -1,7 +1,5 @@ -use crate::processor::utils::loaders::{ - load_account, load_initialized_protocol_fees_vault, load_program_config, -}; -use crate::state::ProgramConfig; +use crate::processor::utils::loaders::{load_account, load_initialized_protocol_fees_vault}; +use crate::state::FeesVault; use solana_program::program_error::ProgramError; use solana_program::rent::Rent; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; @@ -11,9 +9,8 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// Accounts: /// /// 1. `[writable]` protocol fees vault PDA -/// 2. `[]` program config PDA -/// 3. `[writable]` fees receiver PDA -/// 4. `[]` delegation program +/// 2. `[writable]` fees receiver PDA +/// 3. `[]` delegation program /// /// Requirements: /// @@ -29,33 +26,32 @@ pub fn process_protocol_claim_fees( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [fees_vault, program_config_account, fees_receiver, program] = accounts else { + let [fees_vault_account, fees_receiver, _program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; // Check if the admin is signer - load_initialized_protocol_fees_vault(fees_vault, true)?; - load_program_config(program_config_account, *program.key, true)?; + load_initialized_protocol_fees_vault(fees_vault_account, true)?; - let program_config_data = program_config_account.try_borrow_data()?; - let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)?; + let fees_vault_data = fees_vault_account.try_borrow_data()?; + let fees_vault = FeesVault::try_from_bytes_with_discriminator(&fees_vault_data)?; load_account( fees_receiver, - program_config.fees_receiver, + fees_vault.fees_receiver, true, "fees receiver", )?; // Calculate the amount to transfer - let min_rent = Rent::default().minimum_balance(8); - if fees_vault.lamports() < min_rent { + let min_rent = Rent::default().minimum_balance(fees_vault.size_with_discriminator()); + if fees_vault_account.lamports() < min_rent { return Err(ProgramError::InsufficientFunds); } - let amount = fees_vault.lamports() - min_rent; + let amount = fees_vault_account.lamports() - min_rent; // Transfer fees to the admin pubkey - **fees_vault.try_borrow_mut_lamports()? = fees_vault + **fees_vault_account.try_borrow_mut_lamports()? = fees_vault_account .lamports() .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs index 7a0c9d12..ca0b01fe 100644 --- a/src/processor/set_fees_receiver.rs +++ b/src/processor/set_fees_receiver.rs @@ -1,10 +1,11 @@ use crate::args::SetFeesReceiverArgs; use crate::error::DlpError::Unauthorized; use crate::processor::utils::loaders::{ - load_program_config, load_program_upgrade_authority, load_signer, + load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, }; use crate::processor::utils::pda::resize_pda; -use crate::state::ProgramConfig; +use crate::state::discriminator::AccountDiscriminator; +use crate::state::FeesVault; use borsh::BorshDeserialize; use solana_program::msg; use solana_program::program_error::ProgramError; @@ -15,13 +16,12 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// Accounts: /// /// 1. `[signer, writable]` admin account that can set the fees receiver -/// 2. `[writable]` program config PDA -/// 3. `[]` program +/// 2. `[writable]` fees vault PDA +/// 3. `[]` delegation program data /// 4. `[]` system program /// /// Requirements: /// -/// - program config is initialized /// - admin is the protocol config admin /// /// 1. Set the fees receiver in the protocol config @@ -31,9 +31,7 @@ pub fn process_set_fees_receiver( data: &[u8], ) -> ProgramResult { // Load Accounts - let [admin, program_config_account, program, system_program, delegation_program_data] = - accounts - else { + let [admin, fees_vault_account, delegation_program_data, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -52,23 +50,21 @@ pub fn process_set_fees_receiver( return Err(Unauthorized.into()); } - // Check if the program config is initialized - if !load_program_config(program_config_account, *program.key, true)? { - return Err(ProgramError::UninitializedAccount); - } + // Check if the fees vault is initialized + load_initialized_protocol_fees_vault(fees_vault_account, true)?; - // Migrate to the new account structure - let (mut program_config, migrated) = { - let program_config_data = program_config_account.try_borrow_data()?; - match ProgramConfig::try_from_bytes_with_discriminator(&program_config_data) { - Ok(program_config) => (program_config, false), + // Migrate to the new fees vault structure + let (mut fees_vault, migrated) = { + let fees_vault_data = fees_vault_account.try_borrow_data()?; + match FeesVault::try_from_bytes_with_discriminator(&fees_vault_data) { + Ok(fees_vault) => (fees_vault, false), Err(_) => { // Migrating the account - let mut data = program_config_data.to_vec(); - data.extend(Pubkey::default().to_bytes()); - let program_config = ProgramConfig::try_from_bytes_with_discriminator(&data)?; + let mut data = vec![0; FeesVault::default().size_with_discriminator()]; + data[0..8].copy_from_slice(&AccountDiscriminator::FeesVault.to_bytes()); + let fees_vault = FeesVault::try_from_bytes_with_discriminator(&data)?; - (program_config, true) + (fees_vault, true) } } }; @@ -76,17 +72,17 @@ pub fn process_set_fees_receiver( if migrated { resize_pda( admin, - program_config_account, + fees_vault_account, system_program, - program_config.size_with_discriminator(), + fees_vault.size_with_discriminator(), )?; } let args = SetFeesReceiverArgs::try_from_slice(data)?; - program_config.fees_receiver = args.fees_receiver; + fees_vault.fees_receiver = args.fees_receiver; - let mut program_config_data = program_config_account.try_borrow_mut_data()?; - program_config.to_bytes_with_discriminator(&mut program_config_data.as_mut())?; + let mut fees_vault_data = fees_vault_account.try_borrow_mut_data()?; + fees_vault.to_bytes_with_discriminator(&mut fees_vault_data.as_mut())?; Ok(()) } diff --git a/src/state/fees_vault.rs b/src/state/fees_vault.rs new file mode 100644 index 00000000..c7e670c2 --- /dev/null +++ b/src/state/fees_vault.rs @@ -0,0 +1,26 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; + +use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; + +#[derive(BorshSerialize, BorshDeserialize, Default, Debug)] +pub struct FeesVault { + pub fees_receiver: Pubkey, +} + +impl AccountWithDiscriminator for FeesVault { + fn discriminator() -> AccountDiscriminator { + AccountDiscriminator::FeesVault + } +} + +impl FeesVault { + pub fn size_with_discriminator(&self) -> usize { + 8 + 32 + } +} + +impl_to_bytes_with_discriminator_borsh!(FeesVault); +impl_try_from_bytes_with_discriminator_borsh!(FeesVault); diff --git a/src/state/mod.rs b/src/state/mod.rs index 59f89366..c86e3b2f 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,11 +1,13 @@ mod commit_record; mod delegation_metadata; mod delegation_record; +mod fees_vault; mod program_config; mod utils; pub use commit_record::*; pub use delegation_metadata::*; pub use delegation_record::*; +pub use fees_vault::*; pub use program_config::*; pub use utils::*; diff --git a/src/state/utils/discriminator.rs b/src/state/utils/discriminator.rs index 39989b02..44dc9449 100644 --- a/src/state/utils/discriminator.rs +++ b/src/state/utils/discriminator.rs @@ -7,6 +7,7 @@ pub enum AccountDiscriminator { DelegationMetadata = 102, CommitRecord = 101, ProgramConfig = 103, + FeesVault = 104, } impl AccountDiscriminator { diff --git a/tests/integration/programs/test-delegation/Cargo.toml b/tests/integration/programs/test-delegation/Cargo.toml index ad93c701..1681d000 100644 --- a/tests/integration/programs/test-delegation/Cargo.toml +++ b/tests/integration/programs/test-delegation/Cargo.toml @@ -15,6 +15,9 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] idl-build = ["anchor-lang/idl-build"] +anchor-debug = [] +custom-panic = [] +custom-heap = [] [dependencies] anchor-lang = "0.31.1" diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index 0a988132..e203f4de 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -160,6 +160,7 @@ describe("TestDelegation", () => { .delegateTwo() .accounts({ payer: provider.wallet.publicKey, + validator, }) .rpc({ skipPreflight: true }); console.log("Your transaction signature", tx); @@ -496,9 +497,14 @@ describe("TestDelegation", () => { /// Instruction to initialize protocol fees vault function createInitFeesVaultInstruction(payer: web3.PublicKey) { const feesVault = feesVaultPda(); + const delegationProgramData = web3.PublicKey.findProgramAddressSync( + [DELEGATION_PROGRAM_ID.toBuffer()], + BPF_LOADER + )[0]; const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: feesVault, isSigner: false, isWritable: true }, + { pubkey: delegationProgramData, isSigner: false, isWritable: false }, { pubkey: web3.SystemProgram.programId, isSigner: false, @@ -570,17 +576,16 @@ describe("TestDelegation", () => { feesReceiver: web3.PublicKey, program: web3.PublicKey ) { - const programConfig = programConfigPdaFromProgramId(program); + const feesVault = feesVaultPda(); const delegationProgramData = web3.PublicKey.findProgramAddressSync( [DELEGATION_PROGRAM_ID.toBuffer()], BPF_LOADER )[0]; const keys = [ { pubkey: admin, isSigner: true, isWritable: true }, - { pubkey: programConfig, isSigner: false, isWritable: true }, - { pubkey: program, isSigner: false, isWritable: false }, - { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: feesVault, isSigner: false, isWritable: true }, { pubkey: delegationProgramData, isSigner: false, isWritable: false }, + { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, ]; const data = Buffer.from( [16, 0, 0, 0, 0, 0, 0, 0].concat([...feesReceiver.toBytes()]) @@ -599,10 +604,8 @@ describe("TestDelegation", () => { program: web3.PublicKey ) { const feesVault = feesVaultPda(); - const programConfig = programConfigPdaFromProgramId(program); const keys = [ { pubkey: feesVault, isSigner: false, isWritable: true }, - { pubkey: programConfig, isSigner: false, isWritable: true }, { pubkey: feesReceiver, isSigner: false, isWritable: true }, { pubkey: program, isSigner: false, isWritable: false }, ]; diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index c7d3ace0..35635889 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -1,5 +1,6 @@ use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; use dlp::pda::{fees_vault_pda, program_config_from_program_id}; +use dlp::state::FeesVault; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; use solana_program_test::{BanksClient, ProgramTest}; @@ -26,18 +27,16 @@ async fn test_protocol_claim_fees() { assert!(res.is_ok()); // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); assert!(fees_vault_account.is_some()); - assert_eq!( - fees_vault_account.unwrap().lamports, - Rent::default().minimum_balance(8) - ); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); // Assert that the admin account now has the fees let admin_account = banks.get_account(admin.pubkey()).await.unwrap(); assert_eq!( admin_account.unwrap().lamports, - LAMPORTS_PER_SOL * 2 - Rent::default().minimum_balance(8) + LAMPORTS_PER_SOL * 2 - min_rent ); } @@ -59,11 +58,17 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { ); // Setup the fees vault account + let mut buffer = vec![]; + FeesVault { + fees_receiver: admin_keypair.pubkey(), + } + .to_bytes_with_discriminator(&mut buffer) + .unwrap(); program_test.add_account( fees_vault_pda(), Account { lamports: LAMPORTS_PER_SOL, - data: vec![], + data: buffer, owner: dlp::id(), executable: false, rent_epoch: 0, diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index d0f1e3b7..2d0a84aa 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -1,13 +1,15 @@ use std::collections::BTreeSet; +use std::vec; use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; use borsh::{BorshDeserialize, BorshSerialize}; use dlp::pda::{fees_vault_pda, program_config_from_program_id}; use dlp::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; +use dlp::state::FeesVault; use dlp::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, ProgramTest}; use solana_sdk::pubkey::Pubkey; use solana_sdk::{ account::Account, @@ -41,7 +43,7 @@ async fn test_set_fees_receiver() { let fees_receiver = Pubkey::new_unique(); // Set the fees receiver to a new account - let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver, dlp::ID); + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), @@ -64,32 +66,30 @@ async fn test_set_fees_receiver() { assert!(res.is_ok()); // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); assert!(fees_vault_account.is_some()); - assert_eq!( - fees_vault_account.unwrap().lamports, - Rent::default().minimum_balance(8) - ); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); // Assert that the fees receiver account now has the fees let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); assert_eq!( fees_receiver_account.unwrap().lamports, - LAMPORTS_PER_SOL - Rent::default().minimum_balance(8) + LAMPORTS_PER_SOL - min_rent ); } #[tokio::test] async fn test_set_fees_receiver_migration() { // Setup - let (banks, payer, admin, blockhash) = setup_program_test_env(true).await; + let (banks, payer, admin, blockhash) = setup_program_test_env_old_fees_vault(true).await; let fees_vault_pda = fees_vault_pda(); let fees_receiver = Pubkey::new_unique(); // Set the fees receiver to a new account - let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver, dlp::ID); + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); let tx = Transaction::new_signed_with_payer( &[ix], Some(&payer.pubkey()), @@ -112,23 +112,41 @@ async fn test_set_fees_receiver_migration() { assert!(res.is_ok()); // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); assert!(fees_vault_account.is_some()); - assert_eq!( - fees_vault_account.unwrap().lamports, - Rent::default().minimum_balance(8) - ); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); // Assert that the fees receiver account now has the fees let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); assert_eq!( fees_receiver_account.unwrap().lamports, - LAMPORTS_PER_SOL - Rent::default().minimum_balance(8) + LAMPORTS_PER_SOL - min_rent ); } async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair, Hash) { - let mut program_test = ProgramTest::new("dlp", dlp::ID, processor!(dlp::process_instruction)); + // Setup the fees vault account + let mut buffer = vec![]; + FeesVault { + fees_receiver: Pubkey::new_unique(), + } + .to_bytes_with_discriminator(&mut buffer) + .unwrap(); + base_setup_program_test_env(migrate, buffer).await +} + +async fn setup_program_test_env_old_fees_vault( + migrate: bool, +) -> (BanksClient, Keypair, Keypair, Hash) { + base_setup_program_test_env(migrate, vec![]).await +} + +async fn base_setup_program_test_env( + migrate: bool, + fees_vault_data: Vec, +) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); let admin_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); @@ -149,7 +167,7 @@ async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair fees_vault_pda(), Account { lamports: LAMPORTS_PER_SOL, - data: vec![], + data: fees_vault_data, owner: dlp::id(), executable: false, rent_epoch: 0, From ce0a0c517cc884b940c8d57b218a9582435d3b5f Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 28 Oct 2025 23:25:16 +0100 Subject: [PATCH 06/29] test: assert fees vault --- tests/test_set_fees_receiver.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 2d0a84aa..03f54cf1 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -77,6 +77,16 @@ async fn test_set_fees_receiver() { fees_receiver_account.unwrap().lamports, LAMPORTS_PER_SOL - min_rent ); + + // Assert that FeesVault deserializes correctly and stores the right fees_receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); } #[tokio::test] @@ -123,6 +133,16 @@ async fn test_set_fees_receiver_migration() { fees_receiver_account.unwrap().lamports, LAMPORTS_PER_SOL - min_rent ); + + // Assert that FeesVault deserializes correctly and stores the right fees_receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); } async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair, Hash) { From 90e02a13aa0887772c2a4ed17c6e5a483fdf77d9 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 28 Oct 2025 23:28:31 +0100 Subject: [PATCH 07/29] test: wrong receiver --- tests/test_protocol_claim_fees.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 35635889..2eaccf4e 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -40,6 +40,21 @@ async fn test_protocol_claim_fees() { ); } +#[tokio::test] +async fn test_protocol_claim_fees_wrong_receiver() { + // Setup + let (banks, payer, _admin, blockhash) = setup_program_test_env().await; + + // Submit the claim fees tx with wrong receiver + let wrong_receiver = Pubkey::new_unique(); + let ix = dlp::instruction_builder::protocol_claim_fees(wrong_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + + // Assert that the transaction fails because fees_receiver doesn't match stored value + assert!(res.is_err()); +} + async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); From 794d8461c032ccb9370f836a0cf38573576e44a5 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 28 Oct 2025 23:38:48 +0100 Subject: [PATCH 08/29] fix: remove program config changes --- src/processor/close_validator_fees_vault.rs | 2 +- src/processor/whitelist_validator_for_program.rs | 5 +---- src/state/program_config.rs | 3 +-- tests/fixtures/accounts.rs | 3 +-- tests/test_commit_state_with_program_config.rs | 13 +++++-------- tests/test_protocol_claim_fees.rs | 2 +- tests/test_set_fees_receiver.rs | 2 +- 7 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/processor/close_validator_fees_vault.rs b/src/processor/close_validator_fees_vault.rs index bf1c56d7..86541a2a 100644 --- a/src/processor/close_validator_fees_vault.rs +++ b/src/processor/close_validator_fees_vault.rs @@ -15,7 +15,7 @@ use crate::validator_fees_vault_seeds_from_validator; /// /// 0; `[signer]` payer /// 1; `[signer]` admin that controls the vault -/// 2; `[]` delegation program +/// 2; `[]` delegation program data /// 3; `[]` validator_identity /// 4; `[]` validator_fees_vault_pda /// diff --git a/src/processor/whitelist_validator_for_program.rs b/src/processor/whitelist_validator_for_program.rs index 8e1fb67b..d0d51b3f 100644 --- a/src/processor/whitelist_validator_for_program.rs +++ b/src/processor/whitelist_validator_for_program.rs @@ -72,10 +72,7 @@ pub fn process_whitelist_validator_for_program( system_program, authority, )?; - ProgramConfig { - approved_validators: Default::default(), - fees_receiver: *authority.key, - } + ProgramConfig::default() } else { let program_config_data = program_config_account.try_borrow_data()?; ProgramConfig::try_from_bytes_with_discriminator(&program_config_data)? diff --git a/src/state/program_config.rs b/src/state/program_config.rs index eab7f090..21b4aff4 100644 --- a/src/state/program_config.rs +++ b/src/state/program_config.rs @@ -9,7 +9,6 @@ use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; #[derive(BorshSerialize, BorshDeserialize, Default, Debug)] pub struct ProgramConfig { pub approved_validators: BTreeSet, - pub fees_receiver: Pubkey, } impl AccountWithDiscriminator for ProgramConfig { @@ -20,7 +19,7 @@ impl AccountWithDiscriminator for ProgramConfig { impl ProgramConfig { pub fn size_with_discriminator(&self) -> usize { - 8 + 4 + 32 * self.approved_validators.len() + 32 + 8 + 4 + 32 * self.approved_validators.len() } } diff --git a/tests/fixtures/accounts.rs b/tests/fixtures/accounts.rs index e2e78576..6a4c8d68 100644 --- a/tests/fixtures/accounts.rs +++ b/tests/fixtures/accounts.rs @@ -138,10 +138,9 @@ pub fn get_commit_record_account_data(authority: Pubkey) -> Vec { } #[allow(dead_code)] -pub fn create_program_config_data(approved_validator: Pubkey, fees_receiver: Pubkey) -> Vec { +pub fn create_program_config_data(approved_validator: Pubkey) -> Vec { let mut program_config = ProgramConfig { approved_validators: Default::default(), - fees_receiver, }; program_config .approved_validators diff --git a/tests/test_commit_state_with_program_config.rs b/tests/test_commit_state_with_program_config.rs index 048f7164..763681d1 100644 --- a/tests/test_commit_state_with_program_config.rs +++ b/tests/test_commit_state_with_program_config.rs @@ -166,14 +166,11 @@ async fn setup_program_test_env(valid_config: bool) -> (BanksClient, Keypair, Ke ); // Setup the program config - let program_config_data = create_program_config_data( - if valid_config { - validator_keypair.pubkey() - } else { - Keypair::new().pubkey() - }, - validator_keypair.pubkey(), - ); + let program_config_data = create_program_config_data(if valid_config { + validator_keypair.pubkey() + } else { + Keypair::new().pubkey() + }); program_test.add_account( program_config_from_program_id(&DELEGATED_PDA_OWNER_ID), Account { diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 2eaccf4e..0e5dcae5 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -95,7 +95,7 @@ async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { program_config_from_program_id(&dlp::ID), Account { lamports: LAMPORTS_PER_SOL, - data: create_program_config_data(Pubkey::new_unique(), admin_keypair.pubkey()), + data: create_program_config_data(Pubkey::new_unique()), owner: dlp::id(), executable: false, rent_epoch: 0, diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 03f54cf1..50566193 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -208,7 +208,7 @@ async fn base_setup_program_test_env( .unwrap(); bytes } else { - create_program_config_data(Pubkey::new_unique(), admin_keypair.pubkey()) + create_program_config_data(Pubkey::new_unique()) }; program_test.add_account( program_config_from_program_id(&dlp::ID), From 04d31e20ba238135d94251f19da97950a04c8725 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:02:05 +0100 Subject: [PATCH 09/29] feat: prevent fees vault as receiver --- src/processor/protocol_claim_fees.rs | 5 +++++ tests/test_protocol_claim_fees.rs | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index a0edd29c..f99bcb63 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -43,6 +43,11 @@ pub fn process_protocol_claim_fees( "fees receiver", )?; + if fees_receiver.key == fees_vault_account.key { + // Nothing to transfer, or ambiguous aliasing – reject explicitly + return Err(ProgramError::InvalidArgument); + } + // Calculate the amount to transfer let min_rent = Rent::default().minimum_balance(fees_vault.size_with_discriminator()); if fees_vault_account.lamports() < min_rent { diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 0e5dcae5..01de00c2 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -55,6 +55,31 @@ async fn test_protocol_claim_fees_wrong_receiver() { assert!(res.is_err()); } +#[tokio::test] +async fn test_protocol_claim_fees_self() { + // Setup + let (banks, payer, admin, blockhash) = setup_program_test_env().await; + + // Set fees receiver to fees vault + let fees_receiver = fees_vault_pda(); + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + + // Assert that the transaction fails because fees_receiver is the same as the fees vault + assert!(res.is_err()); +} + async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true); From d06db16208b884eaeccc12a3414efa64a5edbcd1 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:06:24 +0100 Subject: [PATCH 10/29] feat: get rent sysvar --- src/processor/protocol_claim_fees.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index f99bcb63..10c23e4f 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -1,8 +1,10 @@ +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, rent::Rent, sysvar::Sysvar, +}; + use crate::processor::utils::loaders::{load_account, load_initialized_protocol_fees_vault}; use crate::state::FeesVault; -use solana_program::program_error::ProgramError; -use solana_program::rent::Rent; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; /// Process request to claim fees from the protocol fees vault /// @@ -49,7 +51,7 @@ pub fn process_protocol_claim_fees( } // Calculate the amount to transfer - let min_rent = Rent::default().minimum_balance(fees_vault.size_with_discriminator()); + let min_rent = Rent::get()?.minimum_balance(fees_vault.size_with_discriminator()); if fees_vault_account.lamports() < min_rent { return Err(ProgramError::InsufficientFunds); } From 58bad681f75ad4497387d4a3379491e8ef99bed0 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:08:07 +0100 Subject: [PATCH 11/29] docs: refer to fees vault --- src/processor/set_fees_receiver.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs index ca0b01fe..effde6d6 100644 --- a/src/processor/set_fees_receiver.rs +++ b/src/processor/set_fees_receiver.rs @@ -22,9 +22,10 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubke /// /// Requirements: /// -/// - admin is the protocol config admin +/// - admin is the program upgrade authority +/// - fees vault is initialized /// -/// 1. Set the fees receiver in the protocol config +/// 1. Set the fees receiver in the [FeesVault] account pub fn process_set_fees_receiver( _program_id: &Pubkey, accounts: &[AccountInfo], From 60552e90dcefed552c9c0a1048f0a2c22d9397a8 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:11:18 +0100 Subject: [PATCH 12/29] feat: use default fees vault on migration --- src/processor/set_fees_receiver.rs | 42 ++++++++++++++---------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs index effde6d6..667fffc6 100644 --- a/src/processor/set_fees_receiver.rs +++ b/src/processor/set_fees_receiver.rs @@ -1,15 +1,20 @@ -use crate::args::SetFeesReceiverArgs; -use crate::error::DlpError::Unauthorized; -use crate::processor::utils::loaders::{ - load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, -}; -use crate::processor::utils::pda::resize_pda; -use crate::state::discriminator::AccountDiscriminator; -use crate::state::FeesVault; use borsh::BorshDeserialize; -use solana_program::msg; -use solana_program::program_error::ProgramError; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::{ + args::SetFeesReceiverArgs, + error::DlpError::Unauthorized, + processor::utils::{ + loaders::{ + load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, + }, + pda::resize_pda, + }, + state::FeesVault, +}; /// Process request to set the fees receiver /// @@ -56,17 +61,10 @@ pub fn process_set_fees_receiver( // Migrate to the new fees vault structure let (mut fees_vault, migrated) = { - let fees_vault_data = fees_vault_account.try_borrow_data()?; - match FeesVault::try_from_bytes_with_discriminator(&fees_vault_data) { - Ok(fees_vault) => (fees_vault, false), - Err(_) => { - // Migrating the account - let mut data = vec![0; FeesVault::default().size_with_discriminator()]; - data[0..8].copy_from_slice(&AccountDiscriminator::FeesVault.to_bytes()); - let fees_vault = FeesVault::try_from_bytes_with_discriminator(&data)?; - - (fees_vault, true) - } + let data = fees_vault_account.try_borrow_data()?; + match FeesVault::try_from_bytes_with_discriminator(&data) { + Ok(fv) => (fv, false), + Err(_) => (FeesVault::default(), true), } }; From 364abb2c8d41c681aa41f57b803f7aa253b69ba1 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:14:51 +0100 Subject: [PATCH 13/29] feat: load system program --- src/processor/set_fees_receiver.rs | 8 +++++++- src/processor/utils/pda.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/processor/set_fees_receiver.rs b/src/processor/set_fees_receiver.rs index 667fffc6..eb610b76 100644 --- a/src/processor/set_fees_receiver.rs +++ b/src/processor/set_fees_receiver.rs @@ -9,7 +9,8 @@ use crate::{ error::DlpError::Unauthorized, processor::utils::{ loaders::{ - load_initialized_protocol_fees_vault, load_program_upgrade_authority, load_signer, + load_initialized_protocol_fees_vault, load_program, load_program_upgrade_authority, + load_signer, }, pda::resize_pda, }, @@ -69,6 +70,11 @@ pub fn process_set_fees_receiver( }; if migrated { + load_program( + system_program, + solana_program::system_program::ID, + "system program", + )?; resize_pda( admin, fees_vault_account, diff --git a/src/processor/utils/pda.rs b/src/processor/utils/pda.rs index c1b470de..6c5c3bc0 100644 --- a/src/processor/utils/pda.rs +++ b/src/processor/utils/pda.rs @@ -88,7 +88,7 @@ pub(crate) fn resize_pda<'a, 'info>( system_program: &'a AccountInfo<'info>, new_size: usize, ) -> Result<(), ProgramError> { - let new_minimum_balance = Rent::default().minimum_balance(new_size); + let new_minimum_balance = Rent::get()?.minimum_balance(new_size); let lamports_diff = new_minimum_balance.saturating_sub(pda.lamports()); invoke( &system_instruction::transfer(payer.key, pda.key, lamports_diff), From 584d0bc15ab474e03333630dfa46ab084ff99b5f Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:22:08 +0100 Subject: [PATCH 14/29] feat: remove unused program --- tests/integration/tests/test-delegation.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index e203f4de..b938d1c7 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -318,11 +318,7 @@ describe("TestDelegation", () => { }); it("Set fees receiver", async () => { - const ix = createSetFeesReceiverInstruction( - admin, - admin, - testDelegation.programId - ); + const ix = createSetFeesReceiverInstruction(admin, admin); const txId = await processInstruction(ix); console.log("Set fees receiver tx:", txId); }); @@ -573,8 +569,7 @@ describe("TestDelegation", () => { /// Instruction to set fees receiver function createSetFeesReceiverInstruction( admin: web3.PublicKey, - feesReceiver: web3.PublicKey, - program: web3.PublicKey + feesReceiver: web3.PublicKey ) { const feesVault = feesVaultPda(); const delegationProgramData = web3.PublicKey.findProgramAddressSync( From bd8dd995b1b93e6aa01c5ed596ea9bf603290684 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:29:24 +0100 Subject: [PATCH 15/29] test: helper for common code --- tests/test_set_fees_receiver.rs | 157 ++++++++++++-------------------- 1 file changed, 59 insertions(+), 98 deletions(-) diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 50566193..ecd51282 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -38,55 +38,7 @@ async fn test_set_fees_receiver() { // Setup let (banks, payer, admin, blockhash) = setup_program_test_env(false).await; - let fees_vault_pda = fees_vault_pda(); - - let fees_receiver = Pubkey::new_unique(); - - // Set the fees receiver to a new account - let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - - // Try claiming to the wrong fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); - let res = banks.process_transaction(tx).await; - assert!(res.is_err()); - - // Claim to the correct fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - - // Assert that fees vault now only have the rent exemption amount - let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); - let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); - assert!(fees_vault_account.is_some()); - assert_eq!(fees_vault_account.unwrap().lamports, min_rent); - - // Assert that the fees receiver account now has the fees - let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); - assert_eq!( - fees_receiver_account.unwrap().lamports, - LAMPORTS_PER_SOL - min_rent - ); - - // Assert that FeesVault deserializes correctly and stores the right fees_receiver - let data = banks - .get_account(fees_vault_pda) - .await - .unwrap() - .unwrap() - .data; - let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); - assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); + helper_test_set_fees_receiver(&banks, &payer, &admin, blockhash).await; } #[tokio::test] @@ -94,55 +46,7 @@ async fn test_set_fees_receiver_migration() { // Setup let (banks, payer, admin, blockhash) = setup_program_test_env_old_fees_vault(true).await; - let fees_vault_pda = fees_vault_pda(); - - let fees_receiver = Pubkey::new_unique(); - - // Set the fees receiver to a new account - let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&payer.pubkey()), - &[&payer, &admin], - blockhash, - ); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - - // Try claiming to the wrong fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); - let res = banks.process_transaction(tx).await; - assert!(res.is_err()); - - // Claim to the correct fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); - let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); - let res = banks.process_transaction(tx).await; - assert!(res.is_ok()); - - // Assert that fees vault now only have the rent exemption amount - let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); - let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); - assert!(fees_vault_account.is_some()); - assert_eq!(fees_vault_account.unwrap().lamports, min_rent); - - // Assert that the fees receiver account now has the fees - let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); - assert_eq!( - fees_receiver_account.unwrap().lamports, - LAMPORTS_PER_SOL - min_rent - ); - - // Assert that FeesVault deserializes correctly and stores the right fees_receiver - let data = banks - .get_account(fees_vault_pda) - .await - .unwrap() - .unwrap() - .data; - let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); - assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); + helper_test_set_fees_receiver(&banks, &payer, &admin, blockhash).await; } async fn setup_program_test_env(migrate: bool) -> (BanksClient, Keypair, Keypair, Hash) { @@ -224,3 +128,60 @@ async fn base_setup_program_test_env( let (banks, payer, blockhash) = program_test.start().await; (banks, payer, admin_keypair, blockhash) } + +async fn helper_test_set_fees_receiver( + banks: &BanksClient, + payer: &Keypair, + admin: &Keypair, + blockhash: Hash, +) { + let fees_vault_pda = fees_vault_pda(); + + let fees_receiver = Pubkey::new_unique(); + + // Set the fees receiver to a new account + let ix = dlp::instruction_builder::set_fees_receiver(admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Try claiming to the wrong fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_err()); + + // Claim to the correct fees receiver + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Assert that fees vault now only have the rent exemption amount + let min_rent = Rent::default().minimum_balance(FeesVault::default().size_with_discriminator()); + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap(); + assert!(fees_vault_account.is_some()); + assert_eq!(fees_vault_account.unwrap().lamports, min_rent); + + // Assert that the fees receiver account now has the fees + let fees_receiver_account = banks.get_account(fees_receiver).await.unwrap(); + assert_eq!( + fees_receiver_account.unwrap().lamports, + LAMPORTS_PER_SOL - min_rent + ); + + // Assert that FeesVault deserializes correctly and stores the right fees_receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(Pubkey::from(vault.fees_receiver), fees_receiver); +} From 74c5e7f00f75c08a570bb139adf58618bfe5dde9 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:31:50 +0100 Subject: [PATCH 16/29] docs: removed irrelevant change --- src/processor/close_validator_fees_vault.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/processor/close_validator_fees_vault.rs b/src/processor/close_validator_fees_vault.rs index 86541a2a..37d5b139 100644 --- a/src/processor/close_validator_fees_vault.rs +++ b/src/processor/close_validator_fees_vault.rs @@ -14,10 +14,9 @@ use crate::validator_fees_vault_seeds_from_validator; /// Accounts: /// /// 0; `[signer]` payer -/// 1; `[signer]` admin that controls the vault -/// 2; `[]` delegation program data -/// 3; `[]` validator_identity -/// 4; `[]` validator_fees_vault_pda +/// 1; `[signer]` admin that is the program upgrade authority +/// 2; `[]` validator_identity +/// 3; `[]` validator_fees_vault_pda /// /// Requirements: /// From e41c86e0b86252fa938d9613a70e8c7118f50e08 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:34:20 +0100 Subject: [PATCH 17/29] test: check state --- tests/test_protocol_claim_fees.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 01de00c2..059e5b1e 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -38,6 +38,16 @@ async fn test_protocol_claim_fees() { admin_account.unwrap().lamports, LAMPORTS_PER_SOL * 2 - min_rent ); + + // Verify FeesVault still stores the admin as receiver + let data = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .data; + let vault = FeesVault::try_from_bytes_with_discriminator(&data).unwrap(); + assert_eq!(vault.fees_receiver, admin.pubkey()); } #[tokio::test] From e42869fd656c112d9128cb4594ecfc3b703dac57 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:38:08 +0100 Subject: [PATCH 18/29] feat: remove unused program account --- src/instruction_builder/protocol_claim_fees.rs | 3 +-- src/processor/protocol_claim_fees.rs | 3 +-- tests/integration/tests/test-delegation.ts | 9 ++------- tests/test_protocol_claim_fees.rs | 6 +++--- tests/test_set_fees_receiver.rs | 4 ++-- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/instruction_builder/protocol_claim_fees.rs b/src/instruction_builder/protocol_claim_fees.rs index 01784e70..2e7f60d7 100644 --- a/src/instruction_builder/protocol_claim_fees.rs +++ b/src/instruction_builder/protocol_claim_fees.rs @@ -6,14 +6,13 @@ use crate::pda::fees_vault_pda; /// Claim the accrued fees from the protocol fees vault. /// See [crate::processor::process_protocol_claim_fees] for docs. -pub fn protocol_claim_fees(fees_receiver: Pubkey, program: Pubkey) -> Instruction { +pub fn protocol_claim_fees(fees_receiver: Pubkey) -> Instruction { let fees_vault_pda = fees_vault_pda(); Instruction { program_id: crate::id(), accounts: vec![ AccountMeta::new(fees_vault_pda, false), AccountMeta::new(fees_receiver, false), - AccountMeta::new_readonly(program, false), ], data: DlpDiscriminator::ProtocolClaimFees.to_vec(), } diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 10c23e4f..0e35f5d2 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -12,7 +12,6 @@ use crate::state::FeesVault; /// /// 1. `[writable]` protocol fees vault PDA /// 2. `[writable]` fees receiver PDA -/// 3. `[]` delegation program /// /// Requirements: /// @@ -28,7 +27,7 @@ pub fn process_protocol_claim_fees( _data: &[u8], ) -> ProgramResult { // Load Accounts - let [fees_vault_account, fees_receiver, _program] = accounts else { + let [fees_vault_account, fees_receiver] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index b938d1c7..303e63e0 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -324,10 +324,7 @@ describe("TestDelegation", () => { }); it("Claim protocol fees", async () => { - const ix = createClaimProtocolFeesVaultInstruction( - admin, - testDelegation.programId - ); + const ix = createClaimProtocolFeesVaultInstruction(admin); const txId = await processInstruction(ix); console.log("Claim protocol fees tx:", txId); }); @@ -595,14 +592,12 @@ describe("TestDelegation", () => { /// Instruction to claim fees from the protocol vault function createClaimProtocolFeesVaultInstruction( - feesReceiver: web3.PublicKey, - program: web3.PublicKey + feesReceiver: web3.PublicKey ) { const feesVault = feesVaultPda(); const keys = [ { pubkey: feesVault, isSigner: false, isWritable: true }, { pubkey: feesReceiver, isSigner: false, isWritable: true }, - { pubkey: program, isSigner: false, isWritable: false }, ]; const data = Buffer.from([12, 0, 0, 0, 0, 0, 0, 0, 0]); const ix = new web3.TransactionInstruction({ diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 059e5b1e..31a02353 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -21,7 +21,7 @@ async fn test_protocol_claim_fees() { let fees_vault_pda = fees_vault_pda(); // Submit the claim fees tx - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); @@ -57,7 +57,7 @@ async fn test_protocol_claim_fees_wrong_receiver() { // Submit the claim fees tx with wrong receiver let wrong_receiver = Pubkey::new_unique(); - let ix = dlp::instruction_builder::protocol_claim_fees(wrong_receiver, dlp::ID); + let ix = dlp::instruction_builder::protocol_claim_fees(wrong_receiver); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; @@ -82,7 +82,7 @@ async fn test_protocol_claim_fees_self() { let res = banks.process_transaction(tx).await; assert!(res.is_ok()); - let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index ecd51282..5f9ea48c 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -151,13 +151,13 @@ async fn helper_test_set_fees_receiver( assert!(res.is_ok()); // Try claiming to the wrong fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey(), dlp::ID); + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_err()); // Claim to the correct fees receiver - let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver, dlp::ID); + let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; assert!(res.is_ok()); From 09760e967246b185e0ea46589fe978beb44b6e10 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 11:51:16 +0100 Subject: [PATCH 19/29] docs: fix comments --- src/processor/protocol_claim_fees.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 0e35f5d2..6712b47d 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -11,7 +11,7 @@ use crate::state::FeesVault; /// Accounts: /// /// 1. `[writable]` protocol fees vault PDA -/// 2. `[writable]` fees receiver PDA +/// 2. `[writable]` fees receiver /// /// Requirements: /// @@ -20,7 +20,7 @@ use crate::state::FeesVault; /// rent exempt /// - fees receiver is the correct one /// -/// 1. Transfer lamports from protocol fees_vault PDA to the admin authority +/// 1. Transfer lamports from the protocol fees vault PDA to the configured fees receiver pub fn process_protocol_claim_fees( _program_id: &Pubkey, accounts: &[AccountInfo], @@ -31,7 +31,7 @@ pub fn process_protocol_claim_fees( return Err(ProgramError::NotEnoughAccountKeys); }; - // Check if the admin is signer + // Validate vault PDA and configured receiver load_initialized_protocol_fees_vault(fees_vault_account, true)?; let fees_vault_data = fees_vault_account.try_borrow_data()?; From 0102b0b794669e044b3fd0ee91f4a5c27b8fb27b Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 12:00:51 +0100 Subject: [PATCH 20/29] test: match specific error --- tests/test_protocol_claim_fees.rs | 24 +++++++++++++++++++++--- tests/test_set_fees_receiver.rs | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 31a02353..72f54dd4 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -3,7 +3,9 @@ use dlp::pda::{fees_vault_pda, program_config_from_program_id}; use dlp::state::FeesVault; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; use solana_sdk::{ account::Account, pubkey::Pubkey, @@ -62,7 +64,15 @@ async fn test_protocol_claim_fees_wrong_receiver() { let res = banks.process_transaction(tx).await; // Assert that the transaction fails because fees_receiver doesn't match stored value - assert!(res.is_err()); + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + )) + ), + "Expected InvalidAccountData error, got {res:?}", + ); } #[tokio::test] @@ -87,7 +97,15 @@ async fn test_protocol_claim_fees_self() { let res = banks.process_transaction(tx).await; // Assert that the transaction fails because fees_receiver is the same as the fees vault - assert!(res.is_err()); + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidArgument) + )) + ), + "Expected InvalidArgument error, got {res:?}", + ); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 5f9ea48c..6af1f282 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -9,8 +9,10 @@ use dlp::state::FeesVault; use dlp::{impl_to_bytes_with_discriminator_borsh, impl_try_from_bytes_with_discriminator_borsh}; use solana_program::rent::Rent; use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; -use solana_program_test::{BanksClient, ProgramTest}; +use solana_program_test::{BanksClient, BanksClientError, ProgramTest}; +use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::TransactionError; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, @@ -154,7 +156,15 @@ async fn helper_test_set_fees_receiver( let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); let res = banks.process_transaction(tx).await; - assert!(res.is_err()); + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + )) + ), + "Expected InvalidAccountData error, got {res:?}", + ); // Claim to the correct fees receiver let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); From 7b1fb92105dce0308e5cd73f3fbf38fb0f469157 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 12:28:21 +0100 Subject: [PATCH 21/29] feat: short circuit no-op transfer --- src/processor/protocol_claim_fees.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 6712b47d..744529c9 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -55,6 +55,9 @@ pub fn process_protocol_claim_fees( return Err(ProgramError::InsufficientFunds); } let amount = fees_vault_account.lamports() - min_rent; + if amount == 0 { + return Ok(()); + } // Transfer fees to the admin pubkey **fees_vault_account.try_borrow_mut_lamports()? = fees_vault_account From 42ce60717848b9387f1fdd30b60d60b56452db74 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 12:30:16 +0100 Subject: [PATCH 22/29] test: assert balance after --- tests/test_protocol_claim_fees.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 72f54dd4..1afe5948 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -73,6 +73,11 @@ async fn test_protocol_claim_fees_wrong_receiver() { ), "Expected InvalidAccountData error, got {res:?}", ); + + // State should be unchanged after failure + let fees_vault_pda = fees_vault_pda(); + let vault_after = banks.get_account(fees_vault_pda).await.unwrap().unwrap(); + assert_eq!(vault_after.lamports, LAMPORTS_PER_SOL); } #[tokio::test] From fc118564d8ed5aa79de7da7f71c2a21d041ff9fa Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 12:34:43 +0100 Subject: [PATCH 23/29] test: unauthorized --- tests/test_set_fees_receiver.rs | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 6af1f282..5940a961 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -3,6 +3,7 @@ use std::vec; use crate::fixtures::{create_program_config_data, TEST_AUTHORITY}; use borsh::{BorshDeserialize, BorshSerialize}; +use dlp::error::DlpError; use dlp::pda::{fees_vault_pda, program_config_from_program_id}; use dlp::state::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; use dlp::state::FeesVault; @@ -43,6 +44,39 @@ async fn test_set_fees_receiver() { helper_test_set_fees_receiver(&banks, &payer, &admin, blockhash).await; } +#[tokio::test] +async fn test_set_fees_receiver_unauthorized() { + // Setup + let (banks, payer, _admin, blockhash) = setup_program_test_env(false).await; + + let fees_receiver: Pubkey = Pubkey::new_unique(); + let unauthorized_admin = Keypair::new(); + + // Set the fees receiver to a new account + let ix = + dlp::instruction_builder::set_fees_receiver(unauthorized_admin.pubkey(), fees_receiver); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &unauthorized_admin], + blockhash, + ); + let res = banks.process_transaction(tx).await; + let expected_code = DlpError::Unauthorized as u32; + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ) + )) if code == expected_code, + ), + "Expected Unauthorized error, got {res:?}", + ); +} + #[tokio::test] async fn test_set_fees_receiver_migration() { // Setup From efce731bbffe1144c29a5883eeb2a1649a96089e Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:10:22 +0100 Subject: [PATCH 24/29] feat: compute rent using actual account --- src/processor/protocol_claim_fees.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 744529c9..e27e2865 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -50,7 +50,7 @@ pub fn process_protocol_claim_fees( } // Calculate the amount to transfer - let min_rent = Rent::get()?.minimum_balance(fees_vault.size_with_discriminator()); + let min_rent = Rent::get()?.minimum_balance(fees_vault_account.data_len()); if fees_vault_account.lamports() < min_rent { return Err(ProgramError::InsufficientFunds); } From 1117c3e786f42f0bc0523e31d0f3033c55082f5d Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:11:12 +0100 Subject: [PATCH 25/29] docs: fix comment --- src/processor/protocol_claim_fees.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index e27e2865..8de4721e 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -59,7 +59,7 @@ pub fn process_protocol_claim_fees( return Ok(()); } - // Transfer fees to the admin pubkey + // Transfer fees to the configured fees receiver **fees_vault_account.try_borrow_mut_lamports()? = fees_vault_account .lamports() .checked_sub(amount) From fa55c6e163b52ef4c4df38d3c0608e2c4e70f51c Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:15:16 +0100 Subject: [PATCH 26/29] test: assert balances unchanged --- tests/test_protocol_claim_fees.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 1afe5948..8b29a305 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -55,7 +55,7 @@ async fn test_protocol_claim_fees() { #[tokio::test] async fn test_protocol_claim_fees_wrong_receiver() { // Setup - let (banks, payer, _admin, blockhash) = setup_program_test_env().await; + let (banks, payer, admin, blockhash) = setup_program_test_env().await; // Submit the claim fees tx with wrong receiver let wrong_receiver = Pubkey::new_unique(); @@ -74,10 +74,18 @@ async fn test_protocol_claim_fees_wrong_receiver() { "Expected InvalidAccountData error, got {res:?}", ); - // State should be unchanged after failure + // Assert that the fees vault still has the initial lamports let fees_vault_pda = fees_vault_pda(); let vault_after = banks.get_account(fees_vault_pda).await.unwrap().unwrap(); assert_eq!(vault_after.lamports, LAMPORTS_PER_SOL); + + // Assert that the admin account still has the initial lamports + let admin_account = banks.get_account(admin.pubkey()).await.unwrap().unwrap(); + assert_eq!(admin_account.lamports, LAMPORTS_PER_SOL); + + // Assert that the fees receiver account still has the initial lamports + let fees_receiver_account = banks.get_account(wrong_receiver).await.unwrap(); + assert_eq!(fees_receiver_account, None); } #[tokio::test] @@ -111,6 +119,15 @@ async fn test_protocol_claim_fees_self() { ), "Expected InvalidArgument error, got {res:?}", ); + + // Assert that the admin account still has the initial lamports + let admin_account = banks.get_account(admin.pubkey()).await.unwrap().unwrap(); + assert_eq!(admin_account.lamports, LAMPORTS_PER_SOL); + + // Assert that the fees vault still has the initial lamports + let fees_vault_pda = fees_vault_pda(); + let fees_vault_account = banks.get_account(fees_vault_pda).await.unwrap().unwrap(); + assert_eq!(fees_vault_account.lamports, LAMPORTS_PER_SOL); } async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { From 16ecbac997a7251a8a1d9dbc09f41f07e75777cd Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:16:47 +0100 Subject: [PATCH 27/29] test: assert state unchanged --- tests/test_set_fees_receiver.rs | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_set_fees_receiver.rs b/tests/test_set_fees_receiver.rs index 5940a961..23fdfd77 100644 --- a/tests/test_set_fees_receiver.rs +++ b/tests/test_set_fees_receiver.rs @@ -186,6 +186,19 @@ async fn helper_test_set_fees_receiver( let res = banks.process_transaction(tx).await; assert!(res.is_ok()); + let vault_before = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .lamports; + let admin_before = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + // Try claiming to the wrong fees receiver let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); @@ -200,6 +213,28 @@ async fn helper_test_set_fees_receiver( "Expected InvalidAccountData error, got {res:?}", ); + // State unchanged + let vault_after = banks + .get_account(fees_vault_pda) + .await + .unwrap() + .unwrap() + .lamports; + let admin_after = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!( + vault_after, vault_before, + "vault lamports changed on failure" + ); + assert_eq!( + admin_after, admin_before, + "admin lamports changed on failure" + ); + // Claim to the correct fees receiver let ix = dlp::instruction_builder::protocol_claim_fees(fees_receiver); let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); From d974afbddeb89eb9394478328d11ff42139a32f6 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:34:21 +0100 Subject: [PATCH 28/29] feat: cache balances --- src/processor/protocol_claim_fees.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/processor/protocol_claim_fees.rs b/src/processor/protocol_claim_fees.rs index 8de4721e..8dc3b279 100644 --- a/src/processor/protocol_claim_fees.rs +++ b/src/processor/protocol_claim_fees.rs @@ -60,13 +60,13 @@ pub fn process_protocol_claim_fees( } // Transfer fees to the configured fees receiver - **fees_vault_account.try_borrow_mut_lamports()? = fees_vault_account - .lamports() + let vault_lamports = fees_vault_account.lamports(); + **fees_vault_account.try_borrow_mut_lamports()? = vault_lamports .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; - **fees_receiver.try_borrow_mut_lamports()? = fees_receiver - .lamports() + let receiver_lamports = fees_receiver.lamports(); + **fees_receiver.try_borrow_mut_lamports()? = receiver_lamports .checked_add(amount) .ok_or(ProgramError::ArithmeticOverflow)?; From 16b560bcb574163129ace98c6c96b161a8d5a833 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 29 Oct 2025 16:36:28 +0100 Subject: [PATCH 29/29] test: edge cases --- tests/test_protocol_claim_fees.rs | 114 ++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tests/test_protocol_claim_fees.rs b/tests/test_protocol_claim_fees.rs index 8b29a305..235762aa 100644 --- a/tests/test_protocol_claim_fees.rs +++ b/tests/test_protocol_claim_fees.rs @@ -130,6 +130,120 @@ async fn test_protocol_claim_fees_self() { assert_eq!(fees_vault_account.lamports, LAMPORTS_PER_SOL); } +#[tokio::test] +async fn test_protocol_claim_fees_noop() { + let (banks, payer, admin, blockhash) = setup_program_test_env().await; + let fees_vault = fees_vault_pda(); + + // First claim: drain to min rent + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + banks.process_transaction(tx).await.unwrap(); + + // Snapshot balances + let vault_before = banks + .get_account(fees_vault) + .await + .unwrap() + .unwrap() + .lamports; + let admin_before = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // Second claim: should be a no-op and return Ok(()) + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + let vault_after = banks + .get_account(fees_vault) + .await + .unwrap() + .unwrap() + .lamports; + let admin_after = banks + .get_account(admin.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + assert_eq!(vault_after, vault_before); + assert_eq!(admin_after, admin_before); +} + +#[tokio::test] +async fn test_protocol_claim_fees_insufficient_funds() { + // Custom env: vault funded below min rent + use solana_program_test::ProgramTest; + use solana_sdk::{account::Account, system_program}; + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let admin = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + program_test.add_account( + admin.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Prepare FeesVault with correct data but underfunded + let mut buf = vec![]; + FeesVault { + fees_receiver: admin.pubkey(), + } + .to_bytes_with_discriminator(&mut buf) + .unwrap(); + let min_rent = Rent::default().minimum_balance(buf.len()); + let underfunded = min_rent.saturating_sub(1); + program_test.add_account( + fees_vault_pda(), + Account { + lamports: underfunded, + data: buf, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Program config (not used by processor here but commonly present) + program_test.add_account( + program_config_from_program_id(&dlp::ID), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_program_config_data(Pubkey::new_unique()), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + + let ix = dlp::instruction_builder::protocol_claim_fees(admin.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!( + matches!( + res, + Err(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InsufficientFunds) + )) + ), + "Expected InsufficientFunds error, got {res:?}", + ); +} + async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { let mut program_test = ProgramTest::new("dlp", dlp::ID, None); program_test.prefer_bpf(true);