Repository navigation
Robinhood Chain: keyed transaction history via Alchemy - #1096
Conversation
9ce231c to
c8d9dcf
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
peachbits
left a comment
There was a problem hiding this comment.
Regarding the third commit, it's not accurate to say that blockscout will handle the internal transactions. Alchemy goes first and if it's successful then blockscout wouldn't be used at all. at best, some internal transaction will come in when alchemy fails and blockscout just happens to work. I don't think it's fixable unless you add a specific fetchInternal method to the network adaptor and create a blockscout adaptor that implements it
|
Agreed: under the waterfall Blockscout was never asked while Alchemy answered, so that sentence in the third commit was wrong. Added NetworkAdapter.fetchInternalTxs and a BlockscoutAdapter that implements it through txlistinternal (it also handles Blockscout's status "2" partially indexed reply and its eth_block_number). EthereumNetwork.acquireTxs calls fetchInternalTxs on every native-asset sync whose fetchTxs rows did not already include internal transfers (tuple.includesInternal) and merges the rows with mergeEdgeTransactions before addTransaction, which replaces a known transaction on an amount change and so needs the external and internal halves as one row. When the internal source fails, the pass is marked partial: only unknown or still-unconfirmed txids are applied and the query window stays open until it recovers. Robinhood now configures blockscout in place of evmscan. Verified in the app on My Robinhood Chain 2 (an EIP-7702 delegate whose deposits are swept by internal calls): the LI.FI payout and the sweeps render, and the engine log reports tokenTxs won by alchemy + blockscout on each sync; screenshots follow in the test-evidence comment. |
c8d9dcf to
ad22eb9
Compare
📸🪓 Test evidence (review round 1: internal transactions)
🪓 HACK-FORCED: wallet2 scene blockscout reachable via ua hack 🪓 HACK-FORCED: wallet2 history with internal sweeps Captured by the agent's in-app test run (build-and-test). |
900c4c3 to
85bf356
Compare
|
Added a browser From a phone on mobile data (T-Mobile,
Without the header the app's internal-transaction fetch fails soft on every sync and Blockscout contributes nothing on this chain. The header follows the precedent in Verified with the compiled adapter and its own header, no local edits, from the build host whose IP Blockscout challenges: |
85bf356 to
41e745d
Compare
d008257 to
c584ab9
Compare
06e05a0 to
d18a742
Compare
peachbits
left a comment
There was a problem hiding this comment.
Re-review of the fetchInternalTxs / blockscout round. The merge design holds up: internal rows reach addTransaction in the same pass as the external rows, includesInternal keeps EvmScan-backed chains and Alchemy-with-internal chains from merging twice, and the reverted-send receipt lookup closes the pending-slider gap. tsc is clean and the PR's tests pass (the one failing test locally, FTM info-server fees, is pre-existing and untouched by this branch). Remaining points are below; none are blockers.
peachbits
left a comment
There was a problem hiding this comment.
Approved, just need to fix some stale documentation
d18a742 to
1cafac4
Compare
|
Both stale documentation passages are rewritten and squashed into the history commit; the two inline threads carry the details. |
1cafac4 to
2f73214
Compare
Blockscout answers a throttled request with status "0", result null and the text in message, which fetchGetEtherscan only ever looked for in result. The reply fell through as an ordinary error, so serialServers never backed off and the engine kept polling at full cadence while history stayed empty.
Transaction history from alchemy_getAssetTransfers (external and erc20,
both directions, pageKey pagination, withMetadata timestamps) and token
balances from alchemy_getTokenBalances. Transfers carry no gas data, so
the wallet's own spends are completed with eth_getTransactionByHash and
eth_getTransactionReceipt from the same endpoint, keeping the amount and
fee conventions of the evmscan path. The {{alchemyApiKey}} placeholder
resolves through the same helper RpcAdapter uses, and a server whose key
is missing drops out of the waterfall instead of failing every call.
Blockscout differs from Etherscan in block height, gastracker, its status "2" partial-index reply, and the throttle text in message. Internal transactions get their own adapter method, fetchInternalTxs, which EthereumNetwork merges into every native-asset sync whose fetchTxs rows did not already carry them (the tuple's includesInternal flag), so a chain whose primary history source cannot see contract-paid ETH still shows it. The merge happens inside one pass because addTransaction replaces a known transaction whose amount changed.
The alchemy adapter leads the waterfall, the Alchemy endpoint joins the RPC server list, and the public Blockscout instance stays as the fallback history source and the only source of internal transactions.
2f73214 to
b091e9a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b091e9a. Configure here.
| const cleanKey = asEthereumInitKeys(match[1]) | ||
| const apiKey = ethEngine.initOptions[cleanKey] | ||
| if (typeof apiKey === 'string') { | ||
| return url.replace(match[0], apiKey) |
There was a problem hiding this comment.
Empty Alchemy key not dropped
Low Severity
resolveServerApiKey treats an empty-string alchemyApiKey as valid and substitutes it into the URL. AlchemyAdapter then keeps that server instead of dropping it, so history hits an unauthenticated Alchemy host and waits on waterfall fallback every sync rather than using Blockscout immediately.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b091e9a. Configure here.







Technical Design Document
robinhood-chain.md
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
Runtime, not merge-order: EdgeApp/edge-react-gui#6150 declares
ROBINHOOD_INITinenvConfig.ts, which is howalchemyApiKeyreaches this plugin. On a gui build without it thealchemyadapter drops its server and Blockscout serves history exactly as before this PR. EdgeApp/edge-currency-accountbased#1062 removes thealchemyApiKeyinit option and must be rescoped to BlockCypher-only before it lands.Description
Asana: https://app.asana.com/0/1215088146871429/1218075069562188
Robinhood Chain (pluginId
robinhood, chain 4663) read transaction history from the public Blockscout instance alone. Blockscout allows 300 requests per minute per client IP, one wallet sync is 9 calls, and past the limit it answers HTTP 200 with{"message":"Too many requests…","result":null,"status":"0"}.EvmScanAdapteronly looked for throttle text inresult, so that reply fell through as an ordinary error: noRateLimitError, no backoff, history stayed empty, and a pending send blocked the slider until a resync.The branch carries these commits:
isEvmScanRateLimitResponserecognizes the Blockscout reply (text inmessage,result: null) alongside the Etherscan variants, soserialServersbacks off. Unit test with the captured JSON.AlchemyAdapter(typealchemy) servesfetchTxsfromalchemy_getAssetTransfers(external for the native asset, erc20 withcontractAddressesper token;fromAddressandtoAddressqueries;pageKeypagination;withMetadatatimestamps) andfetchTokenBalancesfromalchemy_getTokenBalances. Asset transfers carry no gas data, so the wallet's own spends are completed witheth_getTransactionByHashandeth_getTransactionReceiptfrom the same endpoint in one batch, and rows are folded per transaction hash with the amount and fee conventions ofprocessEvmScanTransaction(spend includes the fee, self-send nets to the fee, token spend carriesparentNetworkFee). Written for any Alchemy-served chain. The{{alchemyApiKey}}placeholder resolves through a helper shared withRpcAdapter, and a server whose key is missing drops out of the waterfall. Eight unit tests cover receive, spend, self-send seen from both directions, zero-value contract call, token receive, token spend, a token pull the wallet did not sign, and multi-transfer folding.BlockscoutAdapter(typeblockscout) extends the evmscan adapter for the places Blockscout differs: block height frometh_block_number, no gastracker, and astatus: "2"partially-indexed reply accepted as a success (that reply was failing the cleaner before). Internal transactions get their own adapter method,NetworkAdapter.fetchInternalTxs, whichEthereumNetwork.acquireTxscalls on every native-asset sync whosefetchTxsrows did not already carry internal transfers (tuple.includesInternal) and merges withmergeEdgeTransactionsbeforeaddTransaction, since the engine replaces a known transaction on an amount change and needs the external and internal halves as one row. If the internal source fails, the pass is markedpartial: only unknown or still-unconfirmed txids are applied and the query window stays open. The Blockscout adapter bounds its rate-limit retries (three, 1s/2s/4s) instead of the base adapter's unbounded backoff and pausesfetchInternalTxsfor 60s after a throttle reply, so a throttled public instance costs one partial pass rather than holding the native-asset sync open.alchemyleadsnetworkAdapterConfigs, the Alchemy endpoint joins the RPC server list,blockscoutreplaces theevmscanentry (internal transactions merged into every native sync, history fallback for everything else), andethBalCheckerContractpoints at the eth-balance-checker deployed to0x8950F12786CAE64F94a02733A260ca6FecDaeD7fin block 52107662 (same runtime bytecode as the checker other Edge plugins use at0x7263…, deployed from a key generated for this work).src/docs/robinhood-chain.mdgets a phase 8 entry, decisions 2 and 3 reopened, and the retrospective line that claimed a Blockscout key would remove the limit without a code change corrected. The commit body's claim that Blockscout is the only source of internal transactions predates the fixup; the design doc and this description carry the corrected mechanism.Adapter dispatch is
asyncWaterfall: the first adapter to answer wins each call, so Blockscout is only queried for history while Alchemy is failing;fetchInternalTxsis the one call it serves on every sync. The Alchemy adapter asks for['external', 'internal']on native-asset queries and remembers, per endpoint, a-32602 category is not supportedrejection, re-querying withexternalalone, so a chain where Alchemy serves internal transfers gets them from that adapter without a Blockscout entry.Tested in the app on the iOS simulator with the plugin linked into the gui at PR 6150's head: three Robinhood Chain wallets of one account syncing at once, engine log showing
tokenTxs/tokenBal/blockHeightwon byrobinhood-mainnet.g.alchemy.comon every sync and no throttle line; the wallet's transaction list rendered; a real send of 0.0005389 ETH reached "Transaction Success" (tx0xe2be5265fa03f9704960c31d7aa8bbe5fb839a82d7d9c280690be49fc0d21419); and with the Alchemy host made unreachable in the installed bundle,tokenBal ethBalChecker wontook over balances and Blockscout's throttle was logged asRate limit exceededwith backoff. Screenshots in the test-evidence comment.Review round 1 (2026-09-03): with the linked plugin rebuilt, My Robinhood Chain 2 (an EIP-7702 delegate whose deposits are swept out by internal calls) now lists the LI.FI payout and the sweeps that only
txlistinternalreports, and the engine log showstokenTxs robinhood-mainnet.g.alchemy.com + https://robinhoodchain.blockscout.com wonon each native sync alongside the one-timedoes not serve internal transfers; using external onlyline from the Alchemy adapter. Those frames were captured with a temporaryUser-Agentheader on the Blockscout request. A follow-up measurement from a phone on mobile data showed the challenge is not IP-specific:robinhoodchain.blockscout.comanswers a Cloudflare challenge page to every non-browser agent (the app's CFNetwork and OkHttp agents included) from any IP, whileeth.blockscout.comanswers all of them. The Blockscout adapter therefore now sends a browser user agent on its/apirequests, the same workaroundBlockbookAdapterapplies to Trezor's Blockbook servers, and that header is product code in this PR.Note
Medium Risk
Changes sit on the Ethereum wallet sync and history path (new adapters, internal-tx merge, and global Blockscout rate-limit handling), though Robinhood retains Blockscout/RPC fallbacks and the behavior is covered by targeted tests.
Overview
Robinhood Chain (
robinhood, chain 4663) no longer relies on the public Blockscout API alone for transaction history and token balances. The plugin now leads with a newAlchemyAdapter(alchemy_getAssetTransfers,alchemy_getTokenBalances, with RPC completion for fees on wallet sends) and falls back to a dedicatedBlockscoutAdapterwhen Alchemy is missing or fails.Engine and explorer behavior: Native-asset sync merges internal transactions from Blockscout via a new
fetchInternalTxsadapter hook andmergeInternalTxsinEthereumNetwork, withpartialpasses when internal data fails so history is not overwritten incorrectly.EvmScanAdapternow treats Blockscout’s HTTP 200 “Too many requests” body as aRateLimitError(with backoff capped on Blockscout), accepts Blockscoutstatus: "2"partial index replies, and merges txs case-insensitively across sources.Robinhood config:
robinhoodInforegistersalchemyfirst, swapsevmscanforblockscout, adds Alchemy to RPC servers, and setsethBalCheckerContractfor batched token balances when Alchemy is unavailable. SharedresolveServerApiKeyreplaces duplicated RPC placeholder logic.Unit tests cover Alchemy transfer folding, rate-limit classification, and cross-source tx merging; docs and CHANGELOG describe phase 8 (keyed history).
Reviewed by Cursor Bugbot for commit b091e9a. Bugbot is set up for automated code reviews on this repo. Configure here.