Skip to content

feat: call handler v2#143

Open
taco-paco wants to merge 7 commits intomainfrom
feat/call-handler-v2
Open

feat: call handler v2#143
taco-paco wants to merge 7 commits intomainfrom
feat/call-handler-v2

Conversation

@taco-paco
Copy link
Contributor

@taco-paco taco-paco commented Feb 12, 2026

⚠️ NOTE: Use notes like this to emphasize something important about the PR.

This could include other PRs this PR is built on top of; API breaking changes; reasons for why the PR is on hold; or anything else you would like to draw attention to.

Status Type ⚠️ Core Change Issue
Ready Feature No Link

Problem

What problem are you trying to solve?
The users must be able to get source program from where the action was scheduled

Solution

How did you solve the problem?

Before & After Screenshots

Insert screenshots of example code output

BEFORE:
[insert screenshot here]

AFTER:
[insert screenshot here]

Other changes (e.g. bug fixes, small refactors)

Deploy Notes

Notes regarding deployment of the contained body of work. These should note any
new dependencies, new scripts, etc.

New scripts:

  • script : script details

New dependencies:

  • dependency : dependency details

Summary by CodeRabbit

  • New Features
    • Added call_handler_v2: new instruction flow with size-budget helper and runtime handling, plus public error message constants for escrow validation.
  • Deprecation
    • Legacy call_handler marked deprecated (since 1.1.4); migrate to call_handler_v2.
  • Refactor
    • Internal call handler logic streamlined to reuse centralized error constants and simplified account checks.
  • Tests
    • Added extensive tests and integration scenarios covering finalize/undelegate v2 flows, transfers, and invalid-escrow error cases.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Walkthrough

Adds a new CallHandlerV2 instruction variant, a deprecated legacy builder, a new instruction builder and size-budget helper, a processor implementation performing CPI-style delegation with escrow PDA validation, module re-exports, error constants, and extensive Rust/JS tests exercising finalize and undelegate flows.

Changes

Cohort / File(s) Summary
Discriminator & routing
src/discriminator.rs, src/lib.rs
Added DlpDiscriminator::CallHandlerV2 = 20 and wired it into slow-path dispatch.
Instruction builders
src/instruction_builder/call_handler.rs, src/instruction_builder/call_handler_v2.rs, src/instruction_builder/mod.rs
Marked legacy call_handler deprecated; added new call_handler_v2 builder and call_handler_v2_size_budget, new module and public re-export.
Processor logic
src/processor/call_handler_v2.rs, src/processor/mod.rs, src/processor/call_handler.rs
Added process_call_handler_v2 with signer/escrow validation and CPI via invoke_signed; moved escrow error strings to crate::error and adjusted existing call_handler to import those constants.
Errors
src/error.rs
Added INVALID_ESCROW_PDA and INVALID_ESCROW_OWNER public error message constants.
Tests & test program
tests/test_call_handler_v2.rs, tests/integration/programs/test-delegation/src/lib.rs, tests/integration/tests/test-delegation.ts
Added comprehensive Rust ProgramTest tests and JS integration tests for call_handler_v2 flows; added v2 handlers and account contexts to test delegation program; new helper builders/PDAs in tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • snawaz
  • GabrielePicco
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description identifies a problem ('users must be able to get source program from where the action was scheduled') but fails to explain the solution or provide implementation details, deployment notes, or design rationale for the feature. Add a detailed explanation of how the CallHandlerV2 solution addresses the problem, including design decisions, account structure, and any breaking changes or deployment considerations.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: call handler v2' is concise and accurately reflects the main feature addition across the changeset, including the new discriminator, instruction builder, processor, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/call-handler-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/discriminator.rs`:
- Around line 46-47: The doc comment for the enum variant CallHandlerV2 in
discriminator.rs is missing a trailing period; update the comment text "See
[crate::processor::process_call_handler_v2] for docs" to end with a period so it
matches the style of other variant comments (i.e., "See
[crate::processor::process_call_handler_v2] for docs.").

In `@src/processor/call_handler_v2.rs`:
- Around line 80-87: The log message in the executable check for source_program
prints destination_program.key by mistake; update the msg! call inside the
non-executable branch (the block that returns
Err(ProgramError::InvalidAccountData)) to reference source_program.key instead
of destination_program.key so the error correctly identifies the source program.
- Around line 19-31: The doc comment misnumbers accounts (duplicate index 3) and
makes the PDA origin ambiguous; update the account index list so indices are
sequential and match the named accounts: 0 = `[signer]` validator, 1 = `[]`
validator fee vault, 2 = `[]` destination program, 3 = `[]` source program, 4 =
`[]` escrow_authority, 5 = `[writable]` non-delegated escrow PDA created from
escrow_authority (index 4), 6 = `[readonly/writable]` other accounts needed for
action, 7 = `[readonly/writable]` other accounts needed for action, etc.; also
replace the ambiguous phrase “escrow pda created from 3” with “escrow PDA
created from escrow_authority (index 4)” to keep the documentation consistent
with function semantics in call_handler_v2.

In `@tests/test_call_handler_v2.rs`:
- Around line 186-202: The delegation metadata is being created with the wrong
seed index (using ephemeral_balance_seeds_from_payer!(payer.pubkey(), 0)) while
the actual ephemeral balance PDA (ephemeral_balance_pda) was derived with index
1; update the seeds passed to create_delegation_metadata_data and the
program_test.add_account call so they use
ephemeral_balance_seeds_from_payer!(payer.pubkey(), 1) (or otherwise match the
index used to derive ephemeral_balance_pda) so
delegation_metadata_pda_from_delegated_account(&ephemeral_balance_pda) and
create_delegation_metadata_data(...) use the same seed index.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@tests/integration/programs/test-delegation/src/lib.rs`:
- Around line 120-161: Both v2 handlers duplicate v1 logic; extract the shared
transfer logic into a private helper (e.g.,
perform_escrow_transfer(ctx_accounts: &T, amount: u64) or similar) and call it
from commit_base_action_handler_v2 and undelegate_base_action_handler_v2 to
remove verbatim duplication while preserving the extra counter update in
undelegate_base_action_handler_v2; ensure the helper accepts the context types
or account infos (used in Transfer creation) and that undelegate still
increments the Counter after calling the helper.

In `@tests/integration/tests/test-delegation.ts`:
- Around line 660-696: createCallHandlerV2Instruction relies on Buffer.alloc
zero-fill for bytes 1–7 of the 8-byte discriminator (only byte 0 is set), so
explicitly write the full 8-byte discriminator to the data buffer (e.g., use a
64-bit write like writeBigUInt64LE(BigInt(20), 0) or explicitly zero bytes 1–7
and set byte 0) before writing escrowIndex and lengths; also review the top-up
helper (the function that takes amount at line ~633) and consider using BigInt
for its amount parameter or clearly document/guard against
Number.MAX_SAFE_INTEGER to avoid silent precision loss.
- Around line 630-658: The discriminator for TopUpEphemeralBalance in
createTopUpEphemeralBalanceInstruction only writes the first byte and relies on
Buffer.alloc zero-fill; change it to the same explicit Buffer.from([...])
pattern used elsewhere by encoding all 8 discriminator bytes explicitly (instead
of data.writeUint8(9,0)), then append the u64 amount and u8 index bytes in order
so the final data buffer matches other instruction builders (keep references to
ephemeralBalancePdaFromPayer and DELEGATION_PROGRAM_ID intact).

use solana_program::pubkey::Pubkey;
use solana_program::{msg, system_program};

pub const INVALID_ESCROW_PDA: &str = "invalid escrow pda in CallHandler";
Copy link
Contributor

@GabrielePicco GabrielePicco Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors should be defined in error.rs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const OTHER_ACCOUNTS_OFFSET: usize = 6;

if accounts.len() < OTHER_ACCOUNTS_OFFSET {
return Err(ProgramError::NotEnoughAccountKeys);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems an unnecessary check since it would fail in the below code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, removed

// verify signer is a registered validator
load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?;
// Check if destination program is executable
if !destination_program.executable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary since the CPI would fail anyways

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from both call_handler and call_handler_v2

return Err(ProgramError::InvalidAccountData);
}
// Check if source program is executable
if !source_program.executable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be impossible per construction, also seems uneccesary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but here construction happens elsewhere, while in dlp v2 case the impossiblity by construction indeed works since it is all constructed in one place.

This will work as long as we're the only one who constructing it, which is true at the moment, but in future it could be constructed by any other validator.

// deduce necessary accounts for CPI
let (accounts_meta, handler_accounts): (Vec<AccountMeta>, Vec<AccountInfo>) = other_accounts
.iter()
.chain([source_program, escrow_authority_account, escrow_account])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source_program, in this instruction and also in the handler, could just be a pubkey passed trough the args. We don't need to pass the account. Is there a reason to pass the actual account?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason source_program is passed as an account rather than a pubkey in the args is to preserve the integrator's instruction data (args.data) as an opaque, passthrough blob.

Currently, the integrator (e.g. a game program) schedules an action like:

  let commit_action = FlexiCounterInstruction::CommitActionHandler { amount: PRIZE };
  let data = to_vec(&commit_action);

The dlp handler then forwards args.data verbatim as the instruction data to the destination program's. The destination handler deserializes it directly into its own expected format.

If we moved source_program into the args we'd have a problem: the DLP would need to inject the source_program pubkey into the user's instruction data before the CPI call. But DLP doesn't know the user's serialization format - it could be Borsh, it could be custom, it could be anything. We'd have 2 options here:

  1. Know the user's serialization layout - impossible since it's arbitrary per-program.
  2. Force the user into a wrapper struct like
struct MagicCallHandlerArgs { source_program: Pubkey, data:  Vec<u8> }

which means the destination handler would first have to deserialize the wrapper, then deserialize data again into its actual args. This adds complexity IMO for the User.

By passing source_program as an account the user's args.data stays untouched and opaque, dlp never needs to parse or modify it. The integrator just adds a source_program field to their Anchor accounts struct and then can utilize it or ignore it.

Copy link
Contributor

@GabrielePicco GabrielePicco Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't it just be a new field in ActionArgs ? Instruction data would still be opaque.

Copy link
Contributor Author

@taco-paco taco-paco Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you have to insert/serialize it into user supplied data? We don't call with ActionArgs really we call only with ActionArgs::data.

 let handler_instruction = Instruction {
        program_id: *destination_program.key,
        data: args.data, // I think we would have to insert pubkey here
        accounts: accounts_meta,
    };

What is benefit of passing source as argument?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/processor/call_handler_v2.rs`:
- Around line 66-73: Add the same explicit executability check for
destination_program as is done for source_program: verify
destination_program.executable, emit a clear msg! referencing
destination_program.key (e.g., "{} program is not executable: destination
program"), and return Err(ProgramError::InvalidAccountData) on failure so
callers get a specific early error instead of a generic CPI failure; locate this
change near the existing source_program.executable check in call_handler_v2.rs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants