Skip to content

fix(bitcoin): resolve the counterparty of receive transactions - #372

Open
Battambang wants to merge 6 commits into
mainfrom
feat/btc-receive-counterparty
Open

Battambang wants to merge 6 commits into
mainfrom
feat/btc-receive-counterparty

Conversation

@Battambang

Copy link
Copy Markdown
Contributor

Explanation

What is the current state of things and why does it need to change?

Incoming Bitcoin transactions were displayed without a sender: clients showed the counterparty as "unknown" or "unavailable". Bitcoin inputs only reference a previous outpoint (txid + index) rather than an address, so the funding address cannot be derived from the transaction alone, and mapToTransaction left from empty for receives.

What is the solution?

Resolve the funding addresses through the chain indexer and populate from:

  • EsploraClientAdapter.getTransactionSenders fetches /tx/{txid} over the Esplora REST API, which resolves each input's prevout.scriptpubkey_address in a single request. The configured base URL is normalized by stripping a trailing slash and mempool.space's /v1 prefix, since the transaction endpoint lives at the root.
  • AccountUseCases.resolveTransactionSenders looks up receives only (sends are still displayed as "Sent from Bitcoin Account") with bounded concurrency of 5, so a long history cannot serialize into a multi-minute scan or burst past the indexer's rate limit. Resolution is best-effort: a failure logs at debug level and leaves that transaction without a counterparty rather than failing the caller.
  • mapToTransaction maps the resolved addresses onto from, deduped, with a zero-amount asset so the movement shape stays valid.

Lookups are cached per network:txid, and failures are evicted so a transient rate limit or outage does not permanently suppress a counterparty.

Changes whose purpose might not be obvious

  • The sender lookup only runs for the page returned by getAccountTransactions, so listing cost stays bounded regardless of history size.
  • The version bump in package.json / snap.manifest.json is intentionally omitted: the release process owns it, and this PR only updates the bundled shasum.
  • The from movement carries amount: '0' deliberately. Bitcoin has a single fee paid by the sender, and the received amount is already surfaced through to, so a zero amount keeps the movement shape valid without implying the sender spent nothing.

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

@Battambang
Battambang requested a review from a team as a code owner September 25, 2026 11:18
@Battambang
Battambang deployed to default-branch September 25, 2026 11:18 — with GitHub Actions Active
## Explanation

### What is the current state of things and why does it need to change?

Incoming Bitcoin transactions were displayed without a sender: clients showed
the counterparty as "unknown" or "unavailable". Bitcoin inputs only reference a
previous outpoint (txid + index) rather than an address, so the funding address
cannot be derived from the transaction alone, and `mapToTransaction` left `from`
empty for receives.

### What is the solution?

Resolve the funding addresses through the chain indexer and populate `from`:

- `EsploraClientAdapter.getTransactionSenders` fetches `/tx/{txid}` over the
  Esplora REST API, which resolves each input's `prevout.scriptpubkey_address` in
  a single request. The configured base URL is normalized by stripping a
  trailing slash and mempool.space's `/v1` prefix, since the transaction
  endpoint lives at the root.
- `AccountUseCases.resolveTransactionSenders` looks up receives only (sends are
  still displayed as "Sent from Bitcoin Account") with bounded concurrency of 5,
  so a long history cannot serialize into a multi-minute scan or burst past the
  indexer's rate limit. Resolution is best-effort: a failure logs at debug level
  and leaves that transaction without a counterparty rather than failing the
  caller.
- `mapToTransaction` maps the resolved addresses onto `from`, deduped, with a
  zero-amount asset so the movement shape stays valid.

Lookups are cached per `network:txid`, and failures are evicted so a transient
rate limit or outage does not permanently suppress a counterparty.

### Changes whose purpose might not be obvious

The sender lookup only runs for the page returned by `getAccountTransactions`,
so listing cost stays bounded regardless of history size. The version bump in
`package.json` / `snap.manifest.json` is intentionally omitted: the release
process owns it, and this branch only updates the bundle shasum.

## References

- MetaMask/metamask-extension#45238
… entry

Replace the linked metamask-extension issue with the PR that introduced the
change, so the changelog only points at this repository.
@Battambang
Battambang force-pushed the feat/btc-receive-counterparty branch from 99454e0 to 55285ad Compare September 25, 2026 11:23
@Battambang

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/bitcoin-wallet-snap@3.0.0-preview-39d7fcd
@metamask-previews/snap-networks-utils@1.0.0-preview-39d7fcd
@metamask-previews/solana-wallet-snap@6.0.0-preview-39d7fcd
@metamask-previews/stellar-wallet-snap@1.0.0-preview-39d7fcd
@metamask-previews/tron-wallet-snap@3.2.0-preview-39d7fcd

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.

Copilot review overview

🟡 Changes recommended

Unresolved timeout, cache-race, full-scan, and cross-account concurrency issues block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

This PR resolves sender addresses for Bitcoin receive transactions via Esplora and includes them in transaction mappings and emitted events.

Changes:

  • Added cached, best-effort sender lookups with bounded concurrency.
  • Propagated sender data through synchronization, queries, and events.
  • Updated tests, integration expectations, changelog, and manifest shasum.
File Summary
packages/​bitcoin-wallet-snap/​src/​use-cases/​AccountUseCases.ts Resolves receive transaction senders.
packages/​bitcoin-wallet-snap/​src/​use-cases/​AccountUseCases.test.ts Tests sender resolution and concurrency.
packages/​bitcoin-wallet-snap/​src/​infra/​SnapClientAdapter.ts Passes senders into transaction mappings.
packages/​bitcoin-wallet-snap/​src/​infra/​SnapClientAdapter.test.ts Tests emitted sender data.
packages/​bitcoin-wallet-snap/​src/​infra/​EsploraClientAdapter.ts Fetches and caches Esplora sender data.
packages/​bitcoin-wallet-snap/​src/​infra/​EsploraClientAdapter.test.ts Tests REST lookups, normalization, caching, and errors.
packages/​bitcoin-wallet-snap/​src/​handlers/​mappings.ts Maps sender addresses to receive movements.
packages/​bitcoin-wallet-snap/​src/​handlers/​mappings.test.ts Tests sender mapping and deduplication.
packages/​bitcoin-wallet-snap/​src/​handlers/​KeyringHandler.ts Resolves senders for paginated transactions.
packages/​bitcoin-wallet-snap/​src/​handlers/​KeyringHandler.test.ts Tests paginated sender resolution.
packages/​bitcoin-wallet-snap/​src/​handlers/​CronHandler.ts Forwards senders in transaction events.
packages/​bitcoin-wallet-snap/​src/​handlers/​CronHandler.test.ts Tests event forwarding.
packages/​bitcoin-wallet-snap/​src/​entities/​snap.ts Extends synchronization and client contracts.
packages/​bitcoin-wallet-snap/​src/​entities/​chain.ts Adds the sender-resolution interface.
packages/​bitcoin-wallet-snap/​snap.manifest.json Updates the bundled shasum.
packages/​bitcoin-wallet-snap/​integration-test/​constants.ts Updates receive transaction expectations.
packages/​bitcoin-wallet-snap/​CHANGELOG.md Documents the fix.

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

Comment thread packages/bitcoin-wallet-snap/src/infra/EsploraClientAdapter.ts Outdated
Comment thread packages/bitcoin-wallet-snap/src/use-cases/AccountUseCases.ts
Comment thread packages/bitcoin-wallet-snap/src/use-cases/AccountUseCases.ts
The raw Esplora REST lookup had no timeout, so an indexer that accepted the
connection but stalled would never settle. That promise is awaited while
mapping transactions, so it blocked `synchronize` and suppressed the
transaction event; the existing best-effort handling could not recover
because the request never rejected.

Bound the request with `AbortSignal.timeout` so a stall surfaces as a
rejected lookup, which evicts the cache entry and falls through the existing
best-effort path. The WASM client already applies a retry budget, so the raw
REST call now has an equivalent bound.
@Battambang
Battambang marked this pull request as draft September 25, 2026 12:02
`synchronize` and `fullScan` notify the whole account history at once, so
resolving a counterparty for every receive issued one indexer request per
receive before the event could be emitted. On a long history that risked the
Snap execution deadline, contradicting the bounded-cost behavior of the
paginated listing path.

Resolve at most `SENDER_RESOLUTION_LIMIT` receives on those paths. The cap is
opt-in through a new `limit` argument so the listing path stays uncapped: its
cost is already bounded by the requested page, and capping it would strand
counterparties for older transactions a user can still page back to. The
remainder is resolved lazily via `getAccountTransactions`.
`synchronize` runs for every selected account at once, and each account
resolved its own receives in waves of `SENDER_LOOKUP_CONCURRENCY`. The
budget was applied per call, so a sync with N accounts could reach 5N
indexer requests in flight and risk the indexer's rate limit.

Move the cap onto the `AccountUseCases` instance so the same budget is
shared for every account, keeping the total request count bounded
regardless of how many accounts sync together.
@sonarqubecloud

Copy link
Copy Markdown

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.

Copilot review overview

🟢 Approval recommended

The implementation addresses the prior reliability and concurrency concerns with comprehensive unit and integration coverage.

Review effort: Balanced
Findings: None

Resolved since last review (3)

* @returns The base URL without a trailing slash or `/v1` suffix.
*/
function toEsploraRestUrl(url: string): string {
return url.replace(/\/+$/u, '').replace(/\/v1$/u, '');

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.

Won't fix: the flagged super-linear path (/\/+$/u) requires a slash run followed by more path, which a validated build-time config URL doesn't produce. The input is trusted config, normalized at most 5 times in the constructor. Never per request.

@Battambang

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/bitcoin-wallet-snap@3.0.0-preview-5671415d
@metamask-previews/snap-networks-utils@1.0.0-preview-5671415d
@metamask-previews/solana-wallet-snap@6.0.0-preview-5671415d
@metamask-previews/stellar-wallet-snap@1.0.0-preview-5671415d
@metamask-previews/tron-wallet-snap@3.2.0-preview-5671415d

This branch was successfully deployed

1 active deployment
default-branch — 5671415d Deployed Sep 25, 2026 by Battambang via Determine whether this PR is a release PR #1305
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