Description
The account Transfers tab currently only shows records from balance_transfers, and balance_transfers is only populated from balances.Transfer events.
This misses bridge incoming funds from OmniBridge. OmniBridge incoming funds are emitted as omnibridge.PaidOut + balances.Minted, not as balances.Transfer.
Example account:
46Rgiboa28dB2VbUiwuUAWvQVMBd4cArsnp7nhPP7zF751WD
Decoded AccountId with Heima SS58 prefix 31:
00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335b
Observed chain data:
block: 9716376
extrinsic: 9716376-2
time: 2026-06-04 08:50:24 UTC
971637600002 omnibridge.PayOutVoted
971637600003 balances.Minted
971637600004 omnibridge.PaidOut
The balances.Minted event contains:
account: 00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335b
amount: 0000E8890423C78A0000000000000000
The amount is little-endian and decodes to:
10000000000000000000 = 10 HEI
The account balance is updated correctly, but this bridge movement is not inserted into balance_transfers, so the Transfers tab does not show it.
Goal
Use the compatible approach: extend the existing balance_transfers table/model so bridge incoming records can be stored and displayed in the existing Transfers tab.
Do not introduce a new balance movement table for this task.
Technical Plan
- Extend the balance_transfers schema/model
Extend plugins/balance/model.Transfer with metadata fields:
Category string json:"category" gorm:"size:64;index"
SourceModule string json:"source_module" gorm:"size:64;index"
SourceEvent string json:"source_event" gorm:"size:64;index"
BalanceEvent string json:"balance_event" gorm:"size:64"
Expected values for normal transfers:
category: transfer
source_module: balances
source_event: Transfer
balance_event: Transfer
Expected values for OmniBridge incoming payout:
category: bridge_in
source_module: omnibridge
source_event: PaidOut
balance_event: Minted
Keep the existing fields:
sender
receiver
amount
block_num
block_timestamp
symbol
token_id
extrinsic_index
2. Preserve existing normal transfer indexing
Current logic in plugins/balance/dao/event.go creates balance_transfers rows from balances.Transfer.
Keep this behavior, but populate the new metadata fields for normal transfers:
Category: "transfer",
SourceModule: "balances",
SourceEvent: "Transfer",
BalanceEvent: "Transfer",
Existing transfer records should continue to appear as before.
- Add OmniBridge payout indexing
Create a balance_transfers row when one extrinsic contains both:
omnibridge.PaidOut
balances.Minted
The bridge row should be created from the balances.Minted event data:
receiver = minted account
amount = minted amount
block_num = event block
block_timestamp = block timestamp
extrinsic_index = same extrinsic index
category = bridge_in
source_module = omnibridge
source_event = PaidOut
balance_event = Minted
For sender, use a consistent synthetic value:
sender = omnibridge
If the codebase already has a stable module-account convention, using the actual bridge/module account is also acceptable, but please document the chosen behavior in code.
Important: do not index every balances.Minted. Only create bridge_in rows when a matching omnibridge.PaidOut exists in the same extrinsic_index.
- Implement bridge detection at extrinsic level
Avoid detecting bridge payouts from a single event alone.
plugins/balance/dao/event.go sees one event at a time, so it cannot safely know whether the matching omnibridge.PaidOut exists in the same extrinsic.
Use an extrinsic-level path where all events for the same extrinsic_index are available. For example, internal/observer/go-worker.go already loads:
extrinsic := d.GetExtrinsicsByIndex(ctx, args.ExtrinsicIndex)
events := d.GetEventsByIndex(args.ExtrinsicIndex)
Add bridge detection where the full event list for one extrinsic can be inspected.
Suggested detection algorithm:
for each extrinsic_index:
if events contain omnibridge.PaidOut:
for each balances.Minted event in the same extrinsic:
insert one bridge_in row into balance_transfers
5. Idempotency and duplicate handling
For normal balances.Transfer rows, keep using the existing event id.
For bridge incoming rows, use the balances.Minted event id as balance_transfers.id.
For the example above:
balance_transfers.id = 971637600003
This gives stable idempotency and allows model.IgnoreDuplicate or the existing duplicate-safe insert behavior to prevent duplicate rows during reprocessing/backfill.
If a future extrinsic has multiple balances.Minted events related to one bridge payout, each minted event can create one row using its own event id.
- Backfill historical OmniBridge payout records
Add a backfill/reindex script or command that scans historical indexed events and inserts missing bridge rows.
Suggested backfill algorithm:
- Iterate chain_events* tables.
- Group events by extrinsic_index.
- For each group:
- if it contains omnibridge.PaidOut
- find balances.Minted events in the same group
- insert one balance_transfers row per minted event if missing
- Use the minted event id as balance_transfers.id.
- Use duplicate-safe insert behavior.
At minimum, backfill must insert the example row:
id: 971637600003
block_num: 9716376
extrinsic_index: 9716376-2
sender: omnibridge
receiver: 00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335b
amount: 10000000000000000000
category: bridge_in
source_module: omnibridge
source_event: PaidOut
balance_event: Minted
7. API changes
/api/plugin/balance/transfer should continue returning the same list shape, but rows should now include the new metadata fields:
{
"category": "bridge_in",
"source_module": "omnibridge",
"source_event": "PaidOut",
"balance_event": "Minted"
}
This should be backward compatible because the fields are additive.
- UI changes
Update the Transfers tab to display bridge rows clearly.
Suggested label mapping:
category = transfer -> Transfer
category = bridge_in -> Bridge In
category = bridge_out -> Bridge Out
For bridge incoming rows:
sender display: OmniBridge
receiver display: account
amount display: normal token amount
link target: existing extrinsic/event link behavior
Acceptance Criteria
balance_transfers has the new metadata fields:
category
source_module
source_event
balance_event
Existing balances.Transfer records still appear as before.
Existing and new normal transfer rows are marked with:
category = transfer
source_module = balances
source_event = Transfer
balance_event = Transfer
omnibridge.PaidOut + matching balances.Minted creates a bridge_in row in balance_transfers.
The account page for 46Rgiboa28dB2VbUiwuUAWvQVMBd4cArsnp7nhPP7zF751WD shows the 10 HEI bridge incoming record from 9716376-
2.The UI labels the row as Bridge In or equivalent.
Unrelated balances.Minted, Deposit, Issued, staking rewards, treasury events, and fee refunds are not inserted as bridge transfers.
Backfill inserts the historical row for 9716376-2.
Tests cover:
normal balances.Transfer
omnibridge.PaidOut + balances.Minted
unrelated balances.Minted should not create a balance_transfers row
duplicate-safe reprocessing
API response includes category, source_module, source_event, and balance_event
Description
The account Transfers tab currently only shows records from
balance_transfers, andbalance_transfersis only populated frombalances.Transferevents.This misses bridge incoming funds from OmniBridge. OmniBridge incoming funds are emitted as
omnibridge.PaidOut+balances.Minted, not asbalances.Transfer.Example account:
46Rgiboa28dB2VbUiwuUAWvQVMBd4cArsnp7nhPP7zF751WDDecoded AccountId with Heima SS58 prefix 31:
00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335bObserved chain data:
The balances.Minted event contains:
account: 00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335b
amount: 0000E8890423C78A0000000000000000
The amount is little-endian and decodes to:
10000000000000000000 = 10 HEI
The account balance is updated correctly, but this bridge movement is not inserted into balance_transfers, so the Transfers tab does not show it.
Goal
Use the compatible approach: extend the existing balance_transfers table/model so bridge incoming records can be stored and displayed in the existing Transfers tab.
Do not introduce a new balance movement table for this task.
Technical Plan
Extend plugins/balance/model.Transfer with metadata fields:
Category string
json:"category" gorm:"size:64;index"SourceModule string
json:"source_module" gorm:"size:64;index"SourceEvent string
json:"source_event" gorm:"size:64;index"BalanceEvent string
json:"balance_event" gorm:"size:64"Expected values for normal transfers:
category: transfer
source_module: balances
source_event: Transfer
balance_event: Transfer
Expected values for OmniBridge incoming payout:
category: bridge_in
source_module: omnibridge
source_event: PaidOut
balance_event: Minted
Keep the existing fields:
sender
receiver
amount
block_num
block_timestamp
symbol
token_id
extrinsic_index
2. Preserve existing normal transfer indexing
Current logic in plugins/balance/dao/event.go creates balance_transfers rows from balances.Transfer.
Keep this behavior, but populate the new metadata fields for normal transfers:
Category: "transfer",
SourceModule: "balances",
SourceEvent: "Transfer",
BalanceEvent: "Transfer",
Existing transfer records should continue to appear as before.
Create a balance_transfers row when one extrinsic contains both:
omnibridge.PaidOut
balances.Minted
The bridge row should be created from the balances.Minted event data:
receiver = minted account
amount = minted amount
block_num = event block
block_timestamp = block timestamp
extrinsic_index = same extrinsic index
category = bridge_in
source_module = omnibridge
source_event = PaidOut
balance_event = Minted
For sender, use a consistent synthetic value:
sender = omnibridge
If the codebase already has a stable module-account convention, using the actual bridge/module account is also acceptable, but please document the chosen behavior in code.
Important: do not index every balances.Minted. Only create bridge_in rows when a matching omnibridge.PaidOut exists in the same extrinsic_index.
Avoid detecting bridge payouts from a single event alone.
plugins/balance/dao/event.go sees one event at a time, so it cannot safely know whether the matching omnibridge.PaidOut exists in the same extrinsic.
Use an extrinsic-level path where all events for the same extrinsic_index are available. For example, internal/observer/go-worker.go already loads:
extrinsic := d.GetExtrinsicsByIndex(ctx, args.ExtrinsicIndex)
events := d.GetEventsByIndex(args.ExtrinsicIndex)
Add bridge detection where the full event list for one extrinsic can be inspected.
Suggested detection algorithm:
for each extrinsic_index:
if events contain omnibridge.PaidOut:
for each balances.Minted event in the same extrinsic:
insert one bridge_in row into balance_transfers
5. Idempotency and duplicate handling
For normal balances.Transfer rows, keep using the existing event id.
For bridge incoming rows, use the balances.Minted event id as balance_transfers.id.
For the example above:
balance_transfers.id = 971637600003
This gives stable idempotency and allows model.IgnoreDuplicate or the existing duplicate-safe insert behavior to prevent duplicate rows during reprocessing/backfill.
If a future extrinsic has multiple balances.Minted events related to one bridge payout, each minted event can create one row using its own event id.
Add a backfill/reindex script or command that scans historical indexed events and inserts missing bridge rows.
Suggested backfill algorithm:
At minimum, backfill must insert the example row:
id: 971637600003
block_num: 9716376
extrinsic_index: 9716376-2
sender: omnibridge
receiver: 00f160c0e8fff2d4f00ab03e18dced9f2ac52a6b865cda497a33aee5b3fe335b
amount: 10000000000000000000
category: bridge_in
source_module: omnibridge
source_event: PaidOut
balance_event: Minted
7. API changes
/api/plugin/balance/transfer should continue returning the same list shape, but rows should now include the new metadata fields:
{
"category": "bridge_in",
"source_module": "omnibridge",
"source_event": "PaidOut",
"balance_event": "Minted"
}
This should be backward compatible because the fields are additive.
Update the Transfers tab to display bridge rows clearly.
Suggested label mapping:
category = transfer -> Transfer
category = bridge_in -> Bridge In
category = bridge_out -> Bridge Out
For bridge incoming rows:
sender display: OmniBridge
receiver display: account
amount display: normal token amount
link target: existing extrinsic/event link behavior
Acceptance Criteria
balance_transfers has the new metadata fields:
category
source_module
source_event
balance_event
Existing balances.Transfer records still appear as before.
Existing and new normal transfer rows are marked with:
category = transfer
source_module = balances
source_event = Transfer
balance_event = Transfer
omnibridge.PaidOut + matching balances.Minted creates a bridge_in row in balance_transfers.
The account page for 46Rgiboa28dB2VbUiwuUAWvQVMBd4cArsnp7nhPP7zF751WD shows the 10 HEI bridge incoming record from 9716376-
2.The UI labels the row as Bridge In or equivalent.
Unrelated balances.Minted, Deposit, Issued, staking rewards, treasury events, and fee refunds are not inserted as bridge transfers.
Backfill inserts the historical row for 9716376-2.
Tests cover:
normal balances.Transfer
omnibridge.PaidOut + balances.Minted
unrelated balances.Minted should not create a balance_transfers row
duplicate-safe reprocessing
API response includes category, source_module, source_event, and balance_event