Skip to content

feat: Add pending transaction reconcile max attempt and max age in TransactionService#136

Open
stanleyyconsensys wants to merge 3 commits into
mainfrom
feat/txn-reconcile
Open

feat: Add pending transaction reconcile max attempt and max age in TransactionService#136
stanleyyconsensys wants to merge 3 commits into
mainfrom
feat/txn-reconcile

Conversation

@stanleyyconsensys

Copy link
Copy Markdown
Collaborator

Explanation

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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 bounded eviction for stale pending transactions in snap state by tracking reconcile “not found” attempts and enforcing an age threshold, while ensuring internal-only reconcile metadata is never exposed through keyring APIs.

Changes:

  • Introduces reconcileAttemptCount as snap-internal metadata and strips it on keyring emits/reads.
  • Updates transaction synchronization to increment reconcile attempts when Horizon returns “not found” and persists updated pending state.
  • Evicts pending transactions from snap state only when both max reconcile attempts and max pending age are exceeded (configurable via env/config).

Reviewed changes

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

Show a summary per file
File Description
packages/snap/src/services/transaction/utils.ts Adds helpers for stripping reconcile metadata and determining when pending txs should be dropped.
packages/snap/src/services/transaction/TransactionSynchronizeService.ts Tracks pending txs by id, increments reconcile attempts on Horizon “not found”, strips internal metadata before keyring emit, and persists pending txs.
packages/snap/src/services/transaction/TransactionSynchronizeService.test.ts Adds coverage for reconcile-attempt increment behavior on “not found”.
packages/snap/src/services/transaction/TransactionRepository.ts Stores StellarKeyringTransaction in snap state, strips metadata on read APIs, and evicts stale pending txs in saveMany.
packages/snap/src/services/transaction/TransactionRepository.test.ts Adds eviction/merge behavior tests for reconcile attempts + max age gating.
packages/snap/src/services/transaction/api.ts Introduces StellarKeyringTransaction type (KeyringTransaction + optional reconcile metadata).
packages/snap/src/services/network/exceptions.ts Extends TransactionNotFoundException to carry the transaction hash for callers.
packages/snap/src/config.ts Adds maxReconcileAttempts and maxPendingTransactionAge configuration knobs.
packages/snap/snap.config.ts Wires new env vars into snap build config.
packages/snap/.env.example Documents the new env vars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/snap/src/services/transaction/TransactionSynchronizeService.ts Outdated
Comment thread packages/snap/src/services/transaction/utils.ts Outdated
accountIds: string[],
scope?: KnownCaip2ChainId,
): Promise<KeyringTransaction[]> {
return toKeyringTransactions(

@stanleyyconsensys stanleyyconsensys Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

most of the method, we will re-sharp the result to KeyringTransaction

with that we can avoid too much change on handler side

);
}

async findStellarTransactionsByAccountIds(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is the only method, we return in StellarKeyringTransaction sharp, becoz there is a use case to read the attempt count for a txn

/** All snap-managed accounts on `scope`, keyed by address for SEP-41 receive mapping. */
allAccountsByAddress: Map<StellarAddress, StellarKeyringAccount>;
pendingByAccount: PendingTransactionsByAccount;
pendingTransactionsById: Map<TransactionId, StellarKeyringTransaction>;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

horizon will not return txn that in pending state
Hence pending txn only live when txn send from MM wallet

previously we do make it key by account id, but actually it is not necessary

as each pending txn can only have 1 sender

{};

// Group by transaction id so each on-chain hash is fetched once when shared across accounts.
for (const pendingById of context.pendingByAccount.values()) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is no longer need, as we simplify the structure as a single level MAP

transactionIdsToFetch,
10,
async (transactionId) => ({
transactionId,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is no longer need, as the enquiry transactionId should match the transactionId in onChainTransaction

continue;
}

this.#deletePendingFromState(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no longer need, we can use the Map delete native feature


await this.#transactionRepository.saveMany(
transactions,
transactions.concat(pendingTransactions),

@stanleyyconsensys stanleyyconsensys Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pass both success txn and pending txn to save

it is not going to happen if a txn in both pending and success, so we should safe to not deduplicate

and when we save, we remove the success txn in state + pending txn that is expired (age and max attempt exceed)

this.#logger.debug('Transactions updated event emitted');
}

#getPendingTransactionCount(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

since we simplify the data structure of pending txn , they no longer needed

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