Skip to content

fix(indexer): dedupe deployment inserts by contract address, not just tx hash - #340

Merged
blockchain-maxis merged 2 commits into
blockchain-maxis:mainfrom
ibochielizabeth-spec:fix/deployment-dedupe-by-address
Sep 2, 2026
Merged

fix(indexer): dedupe deployment inserts by contract address, not just tx hash#340
blockchain-maxis merged 2 commits into
blockchain-maxis:mainfrom
ibochielizabeth-spec:fix/deployment-dedupe-by-address

Conversation

@ibochielizabeth-spec

Copy link
Copy Markdown
Contributor

closes #283

Summary

Contract.address is @unique, but the deployment worker's dedupe check only looked at deployTxHash (findFirst({ where: { deployTxHash: txHash } } })). Once a profile can hold several linked wallets, the same contract can be reached from more than one wallet's scan path — each surfacing its own transaction hash — so a tx-hash-only guard doesn't stop a second wallet's scan from attempting to record a contract that's already known under a different wallet.

Changes

  • apps/indexer/src/workers/deployment.ts: added a second dedupe check by address, right after the address is extracted from the transaction (it isn't known yet at the cheap tx-hash-only guard, which still runs first and still short-circuits before fetching the transaction at all whenever possible). The existing upsert (where: { address }, update: {}) was already safe against actually corrupting attribution — an empty update never rewrites walletId/deployerPubkey — but nothing stopped a redundant write attempt, an inflated contractsFound count, or a misleading "new contract" log line on rediscovery, and none of this had any test coverage at all.
  • Made the worker's persistence surface injectable via a DeploymentStore interface (mirroring OperationsStore in operations.ts), defaulting to the real Prisma client so index.ts's call site (runDeploymentWorker(horizon, config)) is unchanged.
  • apps/indexer/src/workers/deployment.test.ts (new): builds real, minimal Soroban V3 result_meta_xdr fixtures using the SDK's own xdr builders, so the dedupe is exercised through the actual extractContractAddress parsing path rather than a stubbed-out one. Covers:
    • the same contract discovered via two different wallets/tx hashes is recorded once, attributed to the first wallet, and not double-counted;
    • re-running over the same fixture doesn't re-attribute or duplicate the row;
    • two genuinely different contracts from two wallets are both recorded correctly;
    • the existing tx-hash-only dedup path (no transaction re-fetch), non-create-contract ops being ignored, and a Horizon failure on one wallet not aborting the others.

Verification

  • pnpm --filter @signet/indexer typecheck / lint / test — all pass (53/53 tests, 6 new).
  • Full workspace pnpm typecheck / pnpm test — unaffected, all pass.

… tx hash

Contract.address is @unique, but the deployment worker's dedupe check
only looked at deployTxHash. Once a profile can hold several linked
wallets, the same contract can be reached from more than one wallet's
scan path — each surfacing its own transaction hash — so a tx-hash-only
guard lets a second wallet's scan attempt to insert a contract that's
already recorded under a different wallet.

Add a second dedupe check by address once the address is known (it
isn't yet at the cheap tx-hash-only guard, which still runs first to
avoid fetching the transaction at all when possible). The upsert itself
was already address-keyed with an empty update, so it couldn't actually
corrupt attribution — but nothing stopped a redundant write attempt,
inflated contractsFound counts, or misleading "new contract" logs, and
none of it was tested.

Made the worker's store injectable (mirroring OperationsStore in
operations.ts, defaulting to the real Prisma client so index.ts's call
site is unchanged) specifically so this could be tested. Added
deployment.test.ts, building real Soroban V3 result_meta_xdr fixtures
via the SDK's own xdr builders so the dedupe is exercised through the
actual contract-address extraction path, not a stubbed-out one.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@ibochielizabeth-spec Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@netlify

netlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploy Preview for stellar-signet ready!

Name Link
🔨 Latest commit 60854f2
🔍 Latest deploy log https://app.netlify.com/projects/stellar-signet/deploys/6a98044fee297c00085117d4
😎 Deploy Preview https://deploy-preview-340--stellar-signet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
signet-web Skipped Skipped Sep 2, 2026 11:11am UTC

blockchain-maxis#334 landed the same DeploymentStore seam this branch introduced, from the
other direction (making the worker testable for mid-life wallet pickup), so
both files conflicted.

deployment.ts:
- keep main's doc comment for the seam, plus this branch's note on why the
  seam also covers the two-wallet dedupe case;
- findFirst takes `{ deployTxHash } | { address }` — this branch's widening,
  which is what the address dedupe needs;
- `store` stays a required parameter (main's index.ts passes prisma
  explicitly), dropping this branch's default;
- keep the address dedupe itself, the point of the PR.

deployment.test.ts: took this branch's file (it carries the three dedupe tests
plus main's other three) and ported main's 'wallet linked between cycles' test
onto its fakeHorizon, which needed pubkey tracking added.

63/63 indexer tests pass; typecheck and lint clean.
@blockchain-maxis
blockchain-maxis merged commit cd1f68b into blockchain-maxis:main Sep 2, 2026
12 checks passed
blockchain-maxis added a commit to escaprt/signet that referenced this pull request Sep 2, 2026
This branch rewrites the deployment worker's single 200-op scan into a
paginated backfill, while blockchain-maxis#334 and blockchain-maxis#340 changed that same scan on main. Merged
so both survive rather than picking a side:

- DeploymentStore gains this branch's wallet.update alongside main's widened
  contract.findFirst (`{ deployTxHash } | { address }`).
- `store` stays a required parameter (index.ts passes prisma explicitly),
  dropping this branch's default.
- blockchain-maxis#340's address dedup is re-added inside handleOperation — the new shared
  handler both the quick-check and backfill paths run through, so it now
  covers more history than the scan it was written for, not less. Without
  this the merge would have silently reverted that fix.

deployment.test.ts: took this branch's file (its fixtures match the paginated
worker) and ported main's coverage onto them — cross-wallet dedup, per-wallet
attribution, the cheap tx-hash guard firing before any transaction fetch,
non-create ops ignored, and mid-life wallet pickup. Its in-memory store's
findFirst also had to learn the address key shape; as written it read
where.deployTxHash unconditionally, so every address lookup would have matched
whichever row had an undefined hash.

Checked the ported dedup test actually bites: stubbing out the address check
fails it (65 pass / 1 fail), so it is a real guard rather than a passing
assertion.

66/66 indexer tests pass; typecheck and lint clean. The migration
(20260830000000_wallet_deployment_backfill) sorts after main's
20260829000000_drop_redundant_unique_indexes, and adds two nullable columns,
so it is safe on an existing database.
blockchain-maxis added a commit to escaprt/signet that referenced this pull request Sep 2, 2026
Both blockchain-maxis#360 (linkDeployWallet) and blockchain-maxis#361 (per-wallet backfill) landed while this
was open, and this PR extends exactly those two files. Everything here is
additive rather than a choice between sides:

- schema.prisma keeps both column sets — blockchain-maxis#361's deploymentCursor /
  deploymentBackfilledAt and this branch's indexRequestedAt.
- account.ts keeps main's WalletSource typing and toWalletSource read guard,
  plus this branch's indexRequestedAt stamping on create and re-link.
- deployment.ts keeps blockchain-maxis#361's paginated quickCheck/backfill split (this branch
  still had the old single 200-op scan, which would have reverted it) and adds
  this branch's indexRequestedAt clearing after each wallet, on success or
  failure.
- DeploymentStore's update now accepts all three fields; findFirst keeps
  blockchain-maxis#340's widened key shape.
- docs/INDEXER.md's worker table describes the paginated scan and the cleared
  request in one row rather than one replacing the other.

deployment.test.ts: took main's file (its fixtures match the paginated worker)
and added index-request coverage on top — cleared after a successful scan,
cleared after a failed one, and untouched when never set. The middle case is
the one worth having: leaving the flag set after a transient Horizon error
would force short ticks for as long as Horizon stayed down.

69/69 indexer and 273/273 web tests pass; typecheck, lint and build clean. The
new migration (20260830010000_wallet_index_request) sorts after blockchain-maxis#361's
20260830000000 and adds one nullable column.
blockchain-maxis added a commit to manchesternews98-jpg/signet that referenced this pull request Sep 4, 2026
Closes blockchain-maxis#394.

blockchain-maxis#279 — the issue this branch was opened against — is already fixed on main by
blockchain-maxis#361, so the wholesale rewrite of deployment.ts is dropped: as authored it
would have reverted the blockchain-maxis#340 address dedup, removed indexRequestedAt handling
(blockchain-maxis#362/blockchain-maxis#334) and left two disagreeing backfill state machines on the Wallet
table (blockchain-maxis#361). What is kept is the two ideas in it that main genuinely does not
have, rebased as a small additive diff.

**Forward catch-up.** Once a wallet is backfilled the worker checked a fixed
newest-200 window each tick. That window is anchored to *now* rather than to
how far the worker actually got, so anything that fell out of it between two
ticks was never examined again — the backward walk is finished and does not
revisit. A deploy script, a busy testnet key, or a long idle interval all
reach it, and the symptom is invisible: the profile just quietly misses
contracts, which is blockchain-maxis#279's complaint arriving by a different route. The new
`Wallet.deploymentWatermark` records the newest operation actually examined,
and the post-backfill check resumes forward from it. A wallet with no
watermark yet reads one bounded newest-first page to establish one, which is
the old behaviour for exactly one tick.

**Per-page persistence.** `deploymentCursor` was written once at the end of a
tick's walk, so a crash discarded up to MAX_PAGES_PER_TICK pages. Both cursor
and watermark are now written after every page. On a wallet thousands of
operations behind, that is the difference between making progress and never
catching up.

The docs claimed "Horizon is always newest-first, so nothing new can be
missed", which is precisely the assumption that fails here; INDEXER.md now
says why the obvious design is wrong instead.

5 tests added, 18 pass. Everything blockchain-maxis#361 established — the dedup, the
indexRequestedAt clearing, the resumable backward walk, the explicit store
seam — is untouched.
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.

Contracts deployed by two wallets on the same profile can duplicate

2 participants