Skip to content

feat(services/graphql): 4/n - simulate classic single-operation transactions in simulateStateChanges - #732

Open
JiahuiWho wants to merge 7 commits into
feat/618-simulate-sep41-tokensfrom
feat/618-simulate-classic-single-op
Open

feat(services/graphql): 4/n - simulate classic single-operation transactions in simulateStateChanges#732
JiahuiWho wants to merge 7 commits into
feat/618-simulate-sep41-tokensfrom
feat/618-simulate-classic-single-op

Conversation

@JiahuiWho

@JiahuiWho JiahuiWho commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What

simulateStateChanges now accepts single-operation classic transactions for five operation types: payment, createAccount, changeTrust, setOptions, and manageData.

Three commits, each reviewable on its own:

  1. The derivation engine (transaction_simulation_classic.go): Derive the classic op simulation outcome steps:
    • list the ledger entries the operation touches (computable from the operation's fields alone),
    • fetch their current state with getLedgerEntries,
    • apply the operation's stated effect to produce before/after entry pairs.
  2. Schema: 14 new Simulated* types for the variants classic operations produce (signer, threshold, account flags, home domain, data entry, and trustline families). Each one mirrors its history twin minus the fields that only exist after a transaction is included in a ledger
  3. Converter: turns the produced state changes into those GraphQL types. Each conversion copies the field logic of the matching history resolver in statechange.resolvers.go.

Why

Design doc: Simulated Transaction State Changes

Known limitations

  • Multi-operation transactions are rejected as UNSUPPORTED_TRANSACTION for now, as are fee-bump classic envelopes.
  • Fees is estimates.

Issue that this PR addresses

#618

Checklist

PR Structure

  • It is not possible to break this PR down into smaller PRs.
  • This PR does not mix refactoring changes with feature changes.
  • This PR's title starts with name of package that is most changed in the PR, or all if the changes are broad or impact many packages.

Thoroughness

  • This PR adds tests for the new functionality or fixes.
  • All updated queries have been tested (refer to this check if the data set returned by the updated query is expected to be same as the original one).

Release

  • This is not a breaking change.
  • This is ready to be tested in development.
  • The new functionality is gated with a feature flag if this is not ready for production.

The network deducts the transaction fee before the operation applies, so the
spendable balance during apply is reduced by the fee when the operation's
source is also the fee payer. The payment and createAccount would-fail
checks now subtract it, closing the borderline case where an account covers
the amount and reserve but not the fee: previously it got a preview for a
transaction that would fail on-chain. Operations with their own source
account are unaffected since a different account pays the fee.
@JiahuiWho
JiahuiWho changed the base branch from main to feat/618-simulate-sep41-tokens September 8, 2026 02:27
@JiahuiWho JiahuiWho changed the title feat(services/graphql): simulate classic single-operation transactions in simulateStateChanges feat(services/graphql): 4/n - simulate classic single-operation transactions in simulateStateChanges Sep 8, 2026
@JiahuiWho
JiahuiWho marked this pull request as ready for review September 8, 2026 13:06
Copilot AI balanced review requested due to automatic review settings September 8, 2026 13:06

Copilot AI left a comment

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.

Pull request overview

Adds classic single-operation transaction simulation to simulateStateChanges.

Changes:

  • Derives ledger changes for five classic operations.
  • Adds corresponding GraphQL state-change variants and converters.
  • Adds service and resolver coverage.

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
internal/services/transaction_simulation.go Integrates classic simulation.
internal/services/transaction_simulation_test.go Updates unsupported-operation coverage.
internal/services/transaction_simulation_classic.go Implements classic derivation.
internal/services/transaction_simulation_classic_test.go Tests supported classic operations.
internal/serve/graphql/schema/simulation.graphqls Defines simulated classic variants.
internal/serve/graphql/schema/queries.graphqls Documents expanded query support.
internal/serve/graphql/resolvers/simulation.go Converts classic state changes.
internal/serve/graphql/resolvers/simulation_test.go Tests new conversions.
internal/serve/graphql/generated/models_gen.go Adds generated GraphQL models.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/services/transaction_simulation_classic.go
Comment thread internal/services/transaction_simulation_classic.go Outdated
Comment thread internal/services/transaction_simulation_classic.go
Comment thread internal/serve/graphql/resolvers/simulation.go
Comment thread internal/services/transaction_simulation_classic.go Outdated
Comment thread internal/services/transaction_simulation_classic.go
Comment thread internal/services/transaction_simulation_classic.go
Copilot AI review requested due to automatic review settings September 8, 2026 17:34

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.

Suppressed comments (2)

internal/services/transaction_simulation_classic.go:467

  • Set-options masks are applied without protocol validation. For example, overlapping setFlags/clearFlags, unknown bits, or clearing flags on an AUTH_IMMUTABLE account are rejected by Core (BAD_FLAGS, UNKNOWN_FLAG, or CANT_CHANGE), but this code synthesizes a successful account update. Validate these constraints before applying either mask.
	if so.ClearFlags != nil {
		after.Flags &^= *so.ClearFlags
	}
	if so.SetFlags != nil {
		after.Flags |= *so.SetFlags

internal/services/transaction_simulation_classic.go:123

  • XDR enforces the 64-byte upper bound but still permits an empty data name. Core returns MANAGE_DATA_INVALID_NAME for that input (internal/indexer/processors/codes.go:390-391), whereas this validation falls through and later creates an empty-name entry. Reject the empty name as a simulation failure.
		// createAccount's starting balance is validated against the minimum
		// reserve in its handler; manageData's field bounds are enforced by
		// XDR decoding (String64 / opaque<64>).

Comment on lines +56 to +60
feeSource, ok := lookupAccount(before, txSource)
if !ok {
return ingest.LedgerTransaction{}, 0, wouldFail("transaction source account %s does not exist", txSource.Address())
}
if accountSpendableBalance(feeSource) < fee {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sequence is skipped deliberately, matching RPC simulateTransaction, which ignores sequence numbers. Wallets simulate before assigning the final sequence, so enforcing it would fail legitimate previews.

Comment on lines +150 to +157
if p.Asset.Type != xdr.AssetTypeAssetTypeNative {
issuer := p.Asset.GetIssuer()
if opSource.Address() != issuer {
keys = append(keys, trustlineLedgerKey(opSource, p.Asset))
}
if dst := p.Destination.ToAccountId(); dst.Address() != issuer {
keys = append(keys, trustlineLedgerKey(dst, p.Asset))
}

@JiahuiWho JiahuiWho Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was removed from the protocol: CAP-0030 (protocol 13, June 2020) deleted PAYMENT_NO_ISSUER and the issuer-existence checks, so a payment of an asset whose issuer was merged succeeds on-chain. Adding the check would make the preview fail transactions the network accept.

On testnet I created an asset, deleted the issuer, and paid the asset between two holders, and it succeeded.

Comment thread internal/indexer/processors/effects.go
Comment on lines +379 to +381
if opSource.Address() == asset.GetIssuer() {
return nil, wouldFail("an issuer cannot trust its own asset %s", assetString(asset))
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In early protocol versions self-trust did succeed as a no-op, and old ledgers contain such transactions, which is why the ingestion code has to handle them.

On the current protocol the network refuses it: I verified on testnet that an issuer submitting changeTrust for its own asset is rejected with CHANGE_TRUST_MALFORMED.

@JiahuiWho
JiahuiWho requested review from Copilot and removed request for Copilot September 8, 2026 20:25
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