Skip to content

Group receipts#70

Open
taco-paco wants to merge 26 commits intomainfrom
feat/transfer-callbacks/group-receipt
Open

Group receipts#70
taco-paco wants to merge 26 commits intomainfrom
feat/transfer-callbacks/group-receipt

Conversation

@taco-paco
Copy link
Copy Markdown
Contributor

@taco-paco taco-paco commented Apr 13, 2026

This PR adds feature of GroupReceipts which accumulate TransferReceipts consisting of data like: amount, signature.

GroupReceipt implemented using crank & action callbacks. Crank - schedules receipt initialization, callback adds creates and adds TransferReceipt

Implementation details

Due to race-condition between crank and callbacks receipt can be initialized in both of them. Since callback doesn't have information on number of splits it just appends data to existing Receipt, sort of like Vec allocating more memory once it runs out of capacity. Arriving crank fully initializes GroupReceipt from there it will be able to eventually be closed as splits are now known

Handling of allocation, deserialization and so on is abstracted via GroupReceiptController

Structure of Receipts

#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct GroupReceiptHeader {
    /// Group ID
    id: u32,
    /// Number of splits
    splits: u32,
    /// How many transfers in this group are still outstanding.
    transfers_completed: u32,
    /// PDA bump for receipt.
    bump: u8,
    /// Reserved for future fields without migration.
    /// Sized to pad header to 24 bytes so items (align 8) start at an 8-byte-aligned offset.
    _reserved: [u8; 11],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct TransferReceipt {
    /// Signature of transfer action, or zeros if signature was absent
    signature: Signature,
    /// Amount transferred in the action
    amount: u64,
    /// Whether the transfer action succeeded (1) or failed (0)
    ok: u8,
    _reserved: [u8; 7],
}

Summary by CodeRabbit

  • New Features

    • Transfer execution callbacks and group receipt tracking to record and finalize grouped transfers, with automatic lifecycle handling (initialize, record, close).
    • Ephemeral account support for temporary storage used during transfer processing.
  • Tests

    • Extensive integration and unit tests plus a test mock for the magic program to validate callback, receipt, and scheduling flows.
  • Chores

    • Updated dependencies and added solana-signature.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@taco-paco has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 22 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 22 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6358b9e-537e-46a3-a455-b59f860031c4

📥 Commits

Reviewing files that changed from the base of the PR and between 736af12 and a5dc242.

📒 Files selected for processing (4)
  • e-token/src/entrypoint.rs
  • e-token/src/processor/deposit_and_delegate_shuttle_ephemeral_ata_with_merge_and_private_transfer.rs
  • e-token/src/processor/mod.rs
  • e-token/src/processor/utils/ephemeral_account.rs

Walkthrough

Adds group-receipt state and end-to-end processing for transfer callbacks: new on-chain structs, controllers, CPI helpers to the magic program, processors to initialize and execute group receipts, transfer-queue wiring to create/schedule receipts, and extensive tests and test mocks. Also updates workspace deps and small formatting/visibility tweaks.

Changes

Cohort / File(s) Summary
Workspace & Manifests
Cargo.toml, e-token/Cargo.toml, e-token-api/Cargo.toml
Switched ephemeral-rollups-pinocchio to a git rev, added solana-signature workspace crate, bumped/added other manifest deps.
API exports & constants
e-token-api/src/lib.rs, e-token-api/src/state/mod.rs
Added internal instruction discriminators and exported new group_receipt state module.
Group receipt state module
e-token-api/src/state/group_receipt.rs
New module defining GroupReceipt, GroupReceiptHeader, TransferReceipt, initialization, mutation, size checks, unsafe view construction, and unit tests.
Processors: execute & initialize
e-token/src/processor/execute_transfer_callback.rs, e-token/src/processor/initialize_group_receipt.rs, e-token/src/processor/mod.rs, e-token/src/entrypoint.rs
New processors for EXECUTE_TRANSFER_CALLBACK and INITIALIZE_GROUP_RECEIPT, entrypoint dispatch, deserialization of magic responses, PDA derivation, validation, and conditional close/update flows; modules added/re-exported.
Queue integration & scheduling
e-token/src/processor/deposit_and_queue_transfer.rs, e-token/src/processor/process_transfer_queue_tick.rs, e-token/src/processor/ensure_transfer_queue_crank.rs
Queue flow extended to create group receipts and schedule an initialize tick via magic crank; callback payload/account construction added; promoted derive_queue_crank_task_id and derive_associated_token_address visibility changes.
Group receipt controller & ephemeral CPI utilities
e-token/src/processor/utils/group_receipt_controller.rs, e-token/src/processor/utils/ephemeral_account.rs, e-token/src/processor/utils/mod.rs
New GroupReceiptController for lifecycle, dynamic allocation, record/close logic; new ephemeral-account CPI helpers and MAGIC_VAULT_ID; re-exported via utils mod.
Callback/CallHandler tweaks
e-token/src/processor/lamports_pda.rs, e-token/src/processor/shuttle_close_schedule.rs, e-token/src/processor/deposit_and_delegate_shuttle_ephemeral_ata_with_merge_and_private_transfer.rs
Explicit callback: None added to several CallHandler constructions; MAGIC_PROGRAM_ID added into private-transfer non-signers and account metas.
Tests & test mocks
e-token/tests/*, e-token/tests/common/magic_mock.rs, e-token/tests/common/mod.rs
Large test additions and refactors: new execute_transfer_callback integration tests, centralized magic_mock test harness to capture CPIs, updates to existing tests to include magic mock and additional account metas.
Misc formatting
compare-metrics/src/main.rs, e-token/tests/rent_pda.rs
Minor formatting/refactor (signature/line-layout) and small test invocation formatting changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • snawaz
  • GabrielePicco
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Group receipts' accurately summarizes the main change in the changeset, which introduces a new GroupReceipt feature for accumulating TransferReceipts.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/transfer-callbacks/group-receipt

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
Copy Markdown
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: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e-token-api/src/state/group_receipt.rs`:
- Around line 41-49: The function from_data_mut currently trusts fields parsed
from GroupReceiptHeader (like transfers_completed and splits) and returns a view
that can cause panics when items() or record_transfer() slice past items_data;
validate header-derived bounds before exposing the view by computing expected
items byte offsets/lengths from header.transfers_completed and header.splits and
ensuring they do not exceed items_data.len(), returning
ProgramError::InvalidAccountData on failure; apply the same bounds checks and
error handling to the other constructor(s)/parser(s) mentioned (the analogous
code around lines 57-93) so any malformed receipt yields InvalidAccountData
instead of panicking.
- Around line 176-177: The two size() helper functions (pub const fn size() ->
usize) call size_of::<Self>() but do not qualify or import it; fix by either
adding an import like use core::mem::size_of; (or use std::mem::size_of;) at the
top of the module or by fully qualifying the call as
core::mem::size_of::<Self>() (or std::mem::size_of::<Self>()), ensuring both
size() helpers reference the qualified/imported symbol so the file compiles.

In `@e-token/src/entrypoint.rs`:
- Line 1: Re-export process_initialize_group_receipt from the processor root so
imports are consistent: add a pub use for process_initialize_group_receipt
(matching how process_execute_transfer_callback is re-exported) in
processor/mod.rs, then remove the explicit use
crate::processor::initialize_group_receipt::process_initialize_group_receipt;
line from entrypoint.rs so entrypoint can rely on use crate::processor::* for
both processors.

In
`@e-token/src/processor/deposit_and_delegate_shuttle_ephemeral_ata_with_merge_and_private_transfer.rs`:
- Line 300: Update the incorrect inline comment on the
MaybeEncryptedAccountMeta::ClearText(compact::AccountMeta::new_readonly(10,
false)) entry to reflect that index 10 is the MAGIC_PROGRAM_ID (magic_program)
rather than shuttle_wallet_ata_info; locate this occurrence in
deposit_and_delegate_shuttle_ephemeral_ata_with_merge_and_private_transfer.rs
and replace the stale comment with one stating "magic_program" or
"MAGIC_PROGRAM_ID" so the account meta annotation matches the actual account
usage.

In `@e-token/src/processor/execute_transfer_callback.rs`:
- Around line 112-114: Replace the manual owner check on queue_info that returns
ProgramError::IllegalOwner with the crate-standard assertion: use the
assert_owner! macro to validate queue_info is owned by crate::ID (i.e., call
assert_owner!(queue_info, &crate::ID) or the crate's expected form) so that
owner mismatches consistently return ProgramError::InvalidAccountOwner; update
the block containing queue_info.owned_by(&crate::ID) accordingly.
- Around line 76-80: The file contains unguarded pinocchio_log::log! calls
(inside the execute_transfer_callback function and elsewhere in this module);
wrap every pinocchio_log::log! invocation and any multi-statement logging blocks
with the crate-wide logging feature guard (use #[cfg(feature = "logging")]
before single-line logs or #[cfg(feature = "logging")] { ... } for blocks) so
logging only compiles when the "logging" feature is enabled; update all
occurrences of the pinocchio_log::log! macro in this file to follow that pattern
and leave other logic unchanged.

In `@e-token/src/processor/utils/ephemeral_account.rs`:
- Around line 55-60: Update the doc comment for the function in
ephemeral_account.rs that currently begins "Creates an ephemeral account via the
magic program." to correctly describe that it "Resizes an ephemeral account via
the magic program" (this is a copy-paste from create_ephemeral_account); also
adjust the summary text and any brief description lines to say "Resizes" instead
of "Creates" and ensure the account references (sponsor, account, vault) and
their roles still match the resize behavior in the function (e.g., account must
be existing and have 0 lamports if applicable).
- Around line 15-16: Update the misleading comment above the
RESIZE_EPHEMERAL_VARIANT constant: it currently refers to
"CreateEphemeralAccount" but should state "ResizeEphemeralAccount" to match the
constant name and variant index; edit the comment text for
RESIZE_EPHEMERAL_VARIANT to correctly describe that it is the bincode variant
index for MagicBlockInstruction::ResizeEphemeralAccount (variant 13).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f3aa62dc-67a2-4f41-bca6-deac342a3328

📥 Commits

Reviewing files that changed from the base of the PR and between f9b0b46 and 9685d39.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • Cargo.toml
  • compare-metrics/src/main.rs
  • e-token-api/Cargo.toml
  • e-token-api/src/lib.rs
  • e-token-api/src/state/group_receipt.rs
  • e-token-api/src/state/mod.rs
  • e-token/Cargo.toml
  • e-token/src/entrypoint.rs
  • e-token/src/processor/deposit_and_delegate_shuttle_ephemeral_ata_with_merge_and_private_transfer.rs
  • e-token/src/processor/deposit_and_queue_transfer.rs
  • e-token/src/processor/ensure_transfer_queue_crank.rs
  • e-token/src/processor/execute_transfer_callback.rs
  • e-token/src/processor/initialize_group_receipt.rs
  • e-token/src/processor/lamports_pda.rs
  • e-token/src/processor/mod.rs
  • e-token/src/processor/process_transfer_queue_tick.rs
  • e-token/src/processor/shuttle_close_schedule.rs
  • e-token/src/processor/utils/ephemeral_account.rs
  • e-token/src/processor/utils/group_receipt_controller.rs
  • e-token/src/processor/utils/mod.rs
  • e-token/tests/rent_pda.rs

Comment thread e-token-api/src/state/group_receipt.rs
Comment thread e-token-api/src/state/group_receipt.rs
Comment thread e-token/src/entrypoint.rs Outdated
Comment on lines +76 to +80
if !response.ok {
if let Ok(value) = core::str::from_utf8(response.error) {
pinocchio_log::log!("Action failed: {}", value);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

echo "== Cargo feature/dependency declarations =="
rg -n 'pinocchio-log|logging' --glob 'Cargo.toml' .

echo
echo "== Logging usage patterns in e-token/src/processor =="
rg -n -C2 'cfg\(feature = "logging"\)|pinocchio_log::log!' e-token/src/processor

Repository: magicblock-labs/ephemeral-spl-token

Length of output: 18055


Standardize logging guards in this file to match the codebase pattern.

execute_transfer_callback.rs has unguarded pinocchio_log::log! calls at lines 76–80, 95–96, 147, and 177–198, while most other processors in e-token/src/processor wrap logging behind #[cfg(feature = "logging")]. Wrap all logging statements in this file with the feature guard for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e-token/src/processor/execute_transfer_callback.rs` around lines 76 - 80, The
file contains unguarded pinocchio_log::log! calls (inside the
execute_transfer_callback function and elsewhere in this module); wrap every
pinocchio_log::log! invocation and any multi-statement logging blocks with the
crate-wide logging feature guard (use #[cfg(feature = "logging")] before
single-line logs or #[cfg(feature = "logging")] { ... } for blocks) so logging
only compiles when the "logging" feature is enabled; update all occurrences of
the pinocchio_log::log! macro in this file to follow that pattern and leave
other logic unchanged.

Comment thread e-token/src/processor/execute_transfer_callback.rs
Comment thread e-token/src/processor/utils/ephemeral_account.rs Outdated
Comment thread e-token/src/processor/utils/ephemeral_account.rs Outdated
Copy link
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e-token/tests/transfer_queue_automation.rs (1)

983-997: 🧹 Nitpick | 🔵 Trivial

Match the queue-crank schedule by content, not by captured[1].

This now passes only while exactly one unrelated ScheduleTask precedes the recurring queue crank. The PR already shifted the index once; another scheduled task will break these tests again even if the queue-crank flow is still correct. Find the task whose instruction targets internal::PROCESS_TRANSFER_QUEUE_TICK (or the expected account tuple) before building scheduled_ix.

Also applies to: 1154-1170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e-token/tests/transfer_queue_automation.rs` around lines 983 - 997, The test
currently assumes the queue-crank ScheduleTask is at captured[1], which is
brittle; instead locate the correct scheduled entry by scanning captured for the
instruction that targets internal::PROCESS_TRANSFER_QUEUE_TICK (or matches the
expected account tuple) and then build scheduled_ix from that found entry;
update both occurrences (around scheduled_ix and the similar block at lines
~1154-1170) to use the discovered index rather than hardcoding captured[1],
referencing the captured vector, Instruction construction, and
internal::PROCESS_TRANSFER_QUEUE_TICK when locating the correct entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e-token/tests/execute_transfer_callback.rs`:
- Around line 158-167: The test fixture always preloads receipt with owner:
PROGRAM so handle_group_receipt never executes the code path guarded by
!group_receipt_info.owned_by(&crate::ID); update tests in
execute_transfer_callback.rs to add at least one case where the receipt account
is created/added with a non-PROGRAM owner (e.g., a different Pubkey) before
calling setup_context()/the test flow so the code path that partially
initializes the receipt when the callback wins the race is exercised; then
assert the resulting receipt header and items (the fields checked elsewhere in
the file after callback handling) match the expected partially-initialized
values to validate the callback-before-crank behavior.

---

Outside diff comments:
In `@e-token/tests/transfer_queue_automation.rs`:
- Around line 983-997: The test currently assumes the queue-crank ScheduleTask
is at captured[1], which is brittle; instead locate the correct scheduled entry
by scanning captured for the instruction that targets
internal::PROCESS_TRANSFER_QUEUE_TICK (or matches the expected account tuple)
and then build scheduled_ix from that found entry; update both occurrences
(around scheduled_ix and the similar block at lines ~1154-1170) to use the
discovered index rather than hardcoding captured[1], referencing the captured
vector, Instruction construction, and internal::PROCESS_TRANSFER_QUEUE_TICK when
locating the correct entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5fce6639-70b6-44b4-9adb-a2dad4f72033

📥 Commits

Reviewing files that changed from the base of the PR and between 9685d39 and 736af12.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • e-token/Cargo.toml
  • e-token/tests/common/magic_mock.rs
  • e-token/tests/common/mod.rs
  • e-token/tests/deposit_and_queue_transfer.rs
  • e-token/tests/execute_transfer_callback.rs
  • e-token/tests/transfer_queue_automation.rs

Comment on lines +67 to +106
fn captured_schedules() -> &'static Mutex<HashMap<Pubkey, Vec<CapturedScheduleTask>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedScheduleTask>>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_cancels() -> &'static Mutex<HashMap<Pubkey, Vec<CapturedCancelTask>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedCancelTask>>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_intent_bundles() -> &'static Mutex<HashMap<Pubkey, Vec<CapturedIntentBundle>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedIntentBundle>>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_action_callbacks() -> &'static Mutex<HashMap<Pubkey, Vec<CapturedActionCallback>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedActionCallback>>>> = OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_ephemeral_creates(
) -> &'static Mutex<HashMap<Pubkey, Vec<CapturedCreateEphemeralAccount>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedCreateEphemeralAccount>>>> =
OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_ephemeral_resizes(
) -> &'static Mutex<HashMap<Pubkey, Vec<CapturedResizeEphemeralAccount>>> {
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedResizeEphemeralAccount>>>> =
OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}

fn captured_ephemeral_closes() -> &'static Mutex<HashMap<Pubkey, Vec<CapturedCloseEphemeralAccount>>>
{
static S: OnceLock<Mutex<HashMap<Pubkey, Vec<CapturedCloseEphemeralAccount>>>> =
OnceLock::new();
S.get_or_init(|| Mutex::new(HashMap::new()))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Captured Magic calls survive caller failure.

These stores are process-global, and process() pushes into them eagerly. If the outer instruction later returns an error, the recorded CPI still remains even though real CPI effects would roll back. That makes take_*/peek_* unreliable for negative-path assertions and can leak stale captures into later test steps unless every failure manually clears them. Please either snapshot/restore the stores around failed transactions or constrain this helper to success-path assertions only.

Also applies to: 223-341

Comment on lines +158 to +167
pt.add_account(
receipt,
SolanaAccount {
lamports: rent.minimum_balance(receipt_data.len()),
data: receipt_data,
owner: PROGRAM,
executable: false,
rent_epoch: 0,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

The callback-before-crank branch still isn't exercised.

setup_context() always preloads receipt with owner: PROGRAM, and every new test reuses that fixture. That means handle_group_receipt never hits the !group_receipt_info.owned_by(&crate::ID) path that creates/partially initializes the receipt when the callback wins the race. Please add one case with a non-PROGRAM receipt account and verify the resulting header/items after the callback.

Also applies to: 243-244, 298-299, 365-366, 429-430

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e-token/tests/execute_transfer_callback.rs` around lines 158 - 167, The test
fixture always preloads receipt with owner: PROGRAM so handle_group_receipt
never executes the code path guarded by
!group_receipt_info.owned_by(&crate::ID); update tests in
execute_transfer_callback.rs to add at least one case where the receipt account
is created/added with a non-PROGRAM owner (e.g., a different Pubkey) before
calling setup_context()/the test flow so the code path that partially
initializes the receipt when the callback wins the race is exercised; then
assert the resulting receipt header and items (the fields checked elsewhere in
the file after callback handling) match the expected partially-initialized
values to validate the callback-before-crank behavior.

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.

1 participant