fix(bitcoin): resolve the counterparty of receive transactions - #372
Battambang wants to merge 6 commits into
Conversation
## 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.
99454e0 to
55285ad
Compare
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
There was a problem hiding this comment.
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
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.
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.
`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.
|
| * @returns The base URL without a trailing slash or `/v1` suffix. | ||
| */ | ||
| function toEsploraRestUrl(url: string): string { | ||
| return url.replace(/\/+$/u, '').replace(/\/v1$/u, ''); |
There was a problem hiding this comment.
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.
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |





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
mapToTransactionleftfromempty for receives.What is the solution?
Resolve the funding addresses through the chain indexer and populate
from:EsploraClientAdapter.getTransactionSendersfetches/tx/{txid}over the Esplora REST API, which resolves each input'sprevout.scriptpubkey_addressin a single request. The configured base URL is normalized by stripping a trailing slash and mempool.space's/v1prefix, since the transaction endpoint lives at the root.AccountUseCases.resolveTransactionSenderslooks 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.mapToTransactionmaps the resolved addresses ontofrom, 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
getAccountTransactions, so listing cost stays bounded regardless of history size.package.json/snap.manifest.jsonis intentionally omitted: the release process owns it, and this PR only updates the bundled shasum.frommovement carriesamount: '0'deliberately. Bitcoin has a single fee paid by the sender, and the received amount is already surfaced throughto, so a zero amount keeps the movement shape valid without implying the sender spent nothing.References
Checklist