Summary
For Tron, getBalanceHistory accounts for only one direction of smart-contract TRX flows. TRX sent into a contract (the top-level call value) is booked as sent, but TRX that comes back from a contract (unwrapping WTRX, removing liquidity, swap payouts, refunds) arrives via internal transactions, which are never counted as received. The result is a one-directional leak that drags the cumulative balance below zero for any DeFi-active address — Trezor Suite renders this as a balance graph that goes deeply negative, which is impossible for a spendable balance.
Root cause: processInternalTransactions is not enabled in configs/coins/tron.json, so bchain.ProcessInternalTransactions defaults to false. Internal data is therefore neither indexed at sync (bchain/coins/tron/tronrpc.go:883) nor read in balance-history accounting (api/worker.go:1792 early-returns).
Reproduction
- Address:
TT2T17KZhoDu47i2E4FWxfG79zdkEWkU9N
- Endpoint: WS/REST
getBalanceHistory
- Observed: cumulative
received - sent bottoms at ≈ −4.96B TRX; totals received 10,591,741,179.20 / sent 15,295,334,567.64 (net −4.7B).
- Reality (from
/api/v2/address/...): the account is fully solvent — 289.5M liquid + 196.6M staked + 344.7M unstaking ≈ 830.7M TRX. It is impossible to have net-sent 4.7B more than ever received; the received side is being undercounted.
The current balance field is correct (read from account state). Only the reconstructed history is wrong.
Evidence — one transaction, dissected
Tx d54a1abc1c21795c2969881d2a7fb4318c3e24df68879100682dd5f791f9a547 (block 83,534,426) — a SunSwap V3 add-liquidity call.
walletsolidity/gettransactioninfobyid shows two native-TRX internal transfers (all other entries have empty callValueInfo):
| internal tx |
caller → to |
native TRX |
600aa8… |
position mgr → WTRX contract |
177,460,640.803707 (our TRX wrapped into the pool) |
dd6b90… |
position mgr → TT2T17KZ… (owner) |
13,245.012561 ← refund back to the address |
Arithmetic confirms the top-level call value is not duplicated in internal_transactions (so enabling processing does not double-count the send):
wrap 177,460,640.803707 + refund 13,245.012561 = 177,473,885.816268 == top-level call value ✅
- Today (internal OFF):
sent 177,473,885.82, received 0 → net −177,473,885.82.
- Correct (internal ON):
sent 177,473,885.82, received 13,245.01 → net −177,460,640.80, exactly the WTRX actually deposited.
This single tx is 100% of the sent in the time: 1781269200 (2026-06-12 13:00 UTC) balance-history bucket. The multi-billion drift comes from the mirror transactions (closing positions / unwrapping WTRX / swap-to-TRX), where call value is 0 (booked sent 0) and the entire principal returns as a native-TRX internal transfer that currently books received 0.
Verification of the existing parser
buildInternalDataFromTronInfos (bchain/coins/tron/tronInternalDataProvider.go) already extracts these transfers correctly (skips empty callValueInfo and TRC-10; ToTronAddressFromAddress handles both the 41… hex form from gettransactioninfobyblocknum and base58 passthrough). processInternalTransactionsForBalanceHistory (api/worker.go:1804-1824) would then credit received += 13,245.01. So the accounting is correct once the flag is on.
Secondary bug exposed by the same data — CREATE misclassification
bchain/coins/tron/tronInternalDataProvider.go:157:
if topType == bchain.CALL && info.ContractAddress != "" {
topType = bchain.CREATE
contractAddr = ToTronAddressFromAddress(info.ContractAddress)
}
Tron populates contract_address for ordinary TriggerSmartContract calls, not just deployments (confirmed in the d54a… response, contract_address = TLSWrv…). So every contract call is reclassified as a contract creation and the called contract is registered as "created in this block" with UnhandledTokenStandard. This does not corrupt balance-history amounts (that path reads only d.Transfers), but it pollutes the contract registry / first-seen + token-standard metadata. It should be gated on a real creation signal (tx being CreateSmartContract, or a create/suicide internal note) rather than merely contract_address != "".
⚠️ A full-history backfill would run this heuristic over every block, so this must be fixed before backfilling.
Proposed fix — no full reindex required
- Enable
processInternalTransactions: true in configs/coins/tron.json (fixes all new blocks). Note: gettransactioninfobyblocknum is already fetched per block for fees/receipts and already contains internal_transactions, so there is no extra sync-time RPC call — the data is currently parsed and discarded.
- Backfill history using the existing primitive rather than a reindex:
ReconnectInternalDataToBlockEthereumType (db/rocksdb_ethereumtype.go:743) patches internal data into an already-synced block and, via existingBlock=true → setAddressTxIndexesToAddressMap, also inserts the address→tx index entries for internal-only counterparties. It's driven by RefetchInternalDataRoutine (api/ethereumtype.go:50), which drains cfBlockInternalDataErrors.
- Since the flag was always off, that error table is empty, so a small range-driver is needed: loop heights →
GetBlock(h) → ReconnectInternalDataToBlockEthereumType(block) (or seed cfBlockInternalDataErrors for the range and trigger RefetchInternalData()).
- This re-fetches each block from the node (RPC load ≈ resync of the range) but writes only internal transfers + affected address indexes — no rebuild of tx data / token transfers / balances / UTXO indexes.
Caveats to validate
- Fix the
:157 CREATE misclassification first.
ReconnectInternalDataToBlockEthereumType was designed for a handful of transient-failure blocks, not full-history backfill. Confirm on a small range that re-adding an address already indexed via its top-level vin/vout does not double its tx count; diff getBalanceHistory + address tx count before/after, then run the full range.
Affected code
configs/coins/tron.json — processInternalTransactions absent → defaults false (bchain/coins/eth/ethrpc.go:274)
bchain/coins/tron/tronrpc.go:883 — internal data built only when flag on
api/worker.go:1792 — balance-history internal accounting gated off
api/worker_balance_history_tron.go — Tron balance-history logic
bchain/coins/tron/tronInternalDataProvider.go:157 — CREATE misclassification
db/rocksdb_ethereumtype.go:743 / api/ethereumtype.go:50 — existing backfill path
Summary
For Tron,
getBalanceHistoryaccounts for only one direction of smart-contract TRX flows. TRX sent into a contract (the top-level call value) is booked assent, but TRX that comes back from a contract (unwrapping WTRX, removing liquidity, swap payouts, refunds) arrives via internal transactions, which are never counted asreceived. The result is a one-directional leak that drags the cumulative balance below zero for any DeFi-active address — Trezor Suite renders this as a balance graph that goes deeply negative, which is impossible for a spendable balance.Root cause:
processInternalTransactionsis not enabled inconfigs/coins/tron.json, sobchain.ProcessInternalTransactionsdefaults tofalse. Internal data is therefore neither indexed at sync (bchain/coins/tron/tronrpc.go:883) nor read in balance-history accounting (api/worker.go:1792early-returns).Reproduction
TT2T17KZhoDu47i2E4FWxfG79zdkEWkU9NgetBalanceHistoryreceived - sentbottoms at ≈ −4.96B TRX; totalsreceived 10,591,741,179.20/sent 15,295,334,567.64(net −4.7B)./api/v2/address/...): the account is fully solvent — 289.5M liquid + 196.6M staked + 344.7M unstaking ≈ 830.7M TRX. It is impossible to have net-sent 4.7B more than ever received; thereceivedside is being undercounted.The current
balancefield is correct (read from account state). Only the reconstructed history is wrong.Evidence — one transaction, dissected
Tx
d54a1abc1c21795c2969881d2a7fb4318c3e24df68879100682dd5f791f9a547(block 83,534,426) — a SunSwap V3 add-liquidity call.walletsolidity/gettransactioninfobyidshows two native-TRX internal transfers (all other entries have emptycallValueInfo):600aa8…dd6b90…TT2T17KZ…(owner)Arithmetic confirms the top-level call value is not duplicated in
internal_transactions(so enabling processing does not double-count the send):sent 177,473,885.82,received 0→ net −177,473,885.82.sent 177,473,885.82,received 13,245.01→ net −177,460,640.80, exactly the WTRX actually deposited.This single tx is 100% of the
sentin thetime: 1781269200(2026-06-12 13:00 UTC) balance-history bucket. The multi-billion drift comes from the mirror transactions (closing positions / unwrapping WTRX / swap-to-TRX), where call value is0(bookedsent 0) and the entire principal returns as a native-TRX internal transfer that currently booksreceived 0.Verification of the existing parser
buildInternalDataFromTronInfos(bchain/coins/tron/tronInternalDataProvider.go) already extracts these transfers correctly (skips emptycallValueInfoand TRC-10;ToTronAddressFromAddresshandles both the41…hex form fromgettransactioninfobyblocknumand base58 passthrough).processInternalTransactionsForBalanceHistory(api/worker.go:1804-1824) would then creditreceived += 13,245.01. So the accounting is correct once the flag is on.Secondary bug exposed by the same data — CREATE misclassification
bchain/coins/tron/tronInternalDataProvider.go:157:Tron populates
contract_addressfor ordinaryTriggerSmartContractcalls, not just deployments (confirmed in thed54a…response,contract_address = TLSWrv…). So every contract call is reclassified as a contract creation and the called contract is registered as "created in this block" withUnhandledTokenStandard. This does not corrupt balance-history amounts (that path reads onlyd.Transfers), but it pollutes the contract registry / first-seen + token-standard metadata. It should be gated on a real creation signal (tx beingCreateSmartContract, or acreate/suicideinternal note) rather than merelycontract_address != "".Proposed fix — no full reindex required
processInternalTransactions: trueinconfigs/coins/tron.json(fixes all new blocks). Note:gettransactioninfobyblocknumis already fetched per block for fees/receipts and already containsinternal_transactions, so there is no extra sync-time RPC call — the data is currently parsed and discarded.ReconnectInternalDataToBlockEthereumType(db/rocksdb_ethereumtype.go:743) patches internal data into an already-synced block and, viaexistingBlock=true→setAddressTxIndexesToAddressMap, also inserts the address→tx index entries for internal-only counterparties. It's driven byRefetchInternalDataRoutine(api/ethereumtype.go:50), which drainscfBlockInternalDataErrors.GetBlock(h)→ReconnectInternalDataToBlockEthereumType(block)(or seedcfBlockInternalDataErrorsfor the range and triggerRefetchInternalData()).Caveats to validate
:157CREATE misclassification first.ReconnectInternalDataToBlockEthereumTypewas designed for a handful of transient-failure blocks, not full-history backfill. Confirm on a small range that re-adding an address already indexed via its top-level vin/vout does not double its tx count; diffgetBalanceHistory+ address tx count before/after, then run the full range.Affected code
configs/coins/tron.json—processInternalTransactionsabsent → defaults false (bchain/coins/eth/ethrpc.go:274)bchain/coins/tron/tronrpc.go:883— internal data built only when flag onapi/worker.go:1792— balance-history internal accounting gated offapi/worker_balance_history_tron.go— Tron balance-history logicbchain/coins/tron/tronInternalDataProvider.go:157— CREATE misclassificationdb/rocksdb_ethereumtype.go:743/api/ethereumtype.go:50— existing backfill path