Skip to content

feat(core): add simulate_charge() read-only preview function #44

Description

@Demilade10

Description

This issue evaluates and, if justified, adds a read-only contract preview for PayStream-owned charge validation. The preview can expose due status and computed charge information without mutating subscription state.

It must not be represented as a guarantee that the real charge() will succeed.

Requirements and Context

Soroban RPC already supports simulating the actual charge() invocation before submission. In the current keeper, prepareTransaction() performs simulation/preparation before signing and sending. Therefore:

  • many deterministic contract failures may already be discovered before a transaction is submitted;
  • simulation of the real charge() includes the external token transfer_from path and is more faithful than a preview that deliberately omits it;
  • an explicit preview still requires constructing/invoking a Soroban contract operation through RPC;
  • preview state can change before the real transaction executes;
  • a separate pre-check adds RPC load and may not reduce fees materially.

The PR must document the exact SDK behavior observed with the pinned version and explain why an explicit contract API is still valuable. Valid reasons may include richer structured UI data, reusable PayStream-owned validation, or cheaper/readable diagnostics—not an unsupported promise of fee elimination.

API design

Evaluate whether simulate_charge is the clearest name. It may be confused with Soroban RPC simulation. A name such as preview_charge may better describe contract-level validation, but preserve the requested name if repository API consistency favors it.

A minimal API may be:

pub fn simulate_charge(
    env: Env,
    subscription_id: u64,
) -> Result<(), Error>

A richer read-only result is preferable if it provides stable client value, for example:

#[contracttype]
#[derive(Clone)]
pub struct ChargePreview {
    pub subscription_id: u64,
    pub plan_id: u64,
    pub due: bool,
    pub next_due: u64,
    pub base_amount: i128,
    pub overage_amount: i128,
    pub total_amount: i128,
    pub allowance_remaining: i128,
    pub token: Address,
    pub merchant: Address,
}

Use only fields available in the merged contract. Missing usage-billing fields must not be invented before #36 lands.

The API must:

  1. Load and validate the same PayStream-owned state used by charge().
  2. Return the same typed errors for missing/inactive/not-due/internal-allowance conditions.
  3. Use the same checked amount calculation as charge().
  4. Perform no token transfer.
  5. Make no persistent, temporary, or instance storage mutation.
  6. Not advance next_due, reduce allowance, reset usage, or alter pending adjustments.
  7. Require no authorization if the read is intentionally public.
  8. Document the data exposed publicly.

Shared logic

Refactor deterministic PayStream-owned validation and calculation into an internal helper used by both functions.

The helper should return validated data required by charge(), not merely (), so the real charge does not reload or recalculate conflicting state.

Do not force external side effects into the shared read helper. Separate:

  • deterministic PayStream validation/calculation;
  • external token/router/oracle calls;
  • post-success state mutation.

The refactor must not change charge() behavior, ordering, errors, authorization, amounts, or atomicity.

External-state limitations

A preview that omits transfer_from cannot conclusively validate:

  • live token-contract allowance;
  • allowance expiration;
  • subscriber token balance;
  • token freeze/clawback or other asset rules;
  • router liquidity/slippage;
  • oracle availability;
  • state changes between preview and execution.

Do not map these limitations to Ok(()) meaning “guaranteed success.” Document that Ok means PayStream's previewed checks passed at the observed ledger state.

If querying token balance/allowance is added, it remains a snapshot and must use the exact token interface safely. It still cannot guarantee later execution.

Keeper integration

Keeper integration is optional and must be justified with measurements or clear behavior.

Before adding it, compare:

  1. existing prepareTransaction() simulation of the real charge;
  2. explicit preview followed by preparation and submission.

Do not add the preview if it merely doubles RPC calls without reducing submitted failures or improving diagnostics. If added:

  • make it configurable;
  • preserve retry/backoff classification;
  • handle preview/execution races;
  • do not skip a charge permanently based on stale preview state;
  • test that retry and confirmation behavior are unchanged.

Suggested Execution

  • Branch name: feat/simulate-charge-preview
  • Files to touch:
    • src/lib.rs — shared deterministic validation/calculation and preview API
    • src/test.rs — parity, read-only, privacy, and regression tests
    • docs/ARCHITECTURE.md — preview semantics and RPC-simulation comparison
    • keeper/keeper.js and keeper tests only if integration is justified
  • Example commit message: feat(core): add read-only charge preview with shared validation

Test and Commit Steps

  1. Verify and document the pinned SDK's prepareTransaction()/simulation behavior.
  2. Define the preview's unique value, name, result type, and public-data exposure.
  3. Regression-test current charge() behavior before refactoring.
  4. Extract deterministic validation/calculation into a shared helper.
  5. Implement the preview without external transfers or state mutation.
  6. Add tests covering:
    • parity for every PayStream-owned error;
    • missing subscription and plan;
    • cancelled subscription;
    • immediately before, exactly at, and after next_due;
    • insufficient internal allowance;
    • base and hybrid amount calculations if feat(core): support hybrid flat-fee plus usage billing model #36 is merged;
    • overflow and invalid configuration;
    • repeated preview calls leave all storage and balances unchanged;
    • successful preview does not prove success when token allowance/balance is insufficient;
    • charge() regression behavior and exact balance/state updates;
    • public read without authorization;
    • preview/execution state change where practical.
  7. If keeper integration is included, add Jest tests proving:
    • no duplicate submissions;
    • no retry-classification changes;
    • preview errors are classified safely;
    • an Ok preview does not bypass real preparation;
    • the feature can be disabled.
  8. Run the complete Rust suite with crate-type = ["rlib"] on Windows if required:
cargo fmt --all -- --check
cargo test
cargo clippy --all-targets --all-features -- -D warnings
  1. If keeper files change:
Set-Location keeper
npm test
Set-Location ..
  1. Restore crate-type = ["cdylib", "rlib"] and build:
stellar contract build --target wasm32v1-none
  1. Commit with:
git add src/lib.rs src/test.rs docs/ARCHITECTURE.md
git add keeper/keeper.js keeper/__tests__
git commit -m "feat(core): add read-only charge preview with shared validation"

Only stage keeper paths if they were changed.

Guidelines

  • Comment with the verified SDK simulation behavior and proposed preview value/result shape to request assignment.
  • Do not start or open a PR until assigned by a maintainer.
  • The PR description must include Closes #<issue-number>.
  • Do not claim preview success guarantees charge success.
  • Do not duplicate deterministic validation logic.
  • Do not change externally observable charge() behavior.
  • Do not add keeper RPC calls without documenting their benefit.
  • Preserve retry, backoff, confirmation, and separate token-approval behavior.
  • Do not renumber existing contract errors.
  • Do not upgrade Soroban SDK from 27.0.3.
  • Follow Conventional Commits and repository branch conventions.

Complexity

High (200 pts)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions