Skip to content

[explorer/puller] feat: sync Symbol confirmed transactions and persist mosaic/address rows#2381

Merged
daoka merged 17 commits into
devfrom
explorer/backend/symbol-transactions-puller
Jul 9, 2026
Merged

[explorer/puller] feat: sync Symbol confirmed transactions and persist mosaic/address rows#2381
daoka merged 17 commits into
devfrom
explorer/backend/symbol-transactions-puller

Conversation

@daoka

@daoka daoka commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add symbol_transactions / symbol_transaction_mosaics / symbol_transaction_addresses tables and puller/model/symbol/Transaction.py to persist confirmed transactions from Symbol Node's /transactions/confirmed endpoint, covering both top-level transactions identified by hash and embedded transactions inside aggregates identified by aggregate_hash + embedded_index.
  • For each transaction type, extract the mosaics and addresses it involves into dedicated child tables so they're searchable without parsing the raw payload. For example, a transfer produces a signer/recipient address pair and a transfer-role mosaic row per mosaic sent; an aggregate produces a cosignatory address row per cosigner. These extractions, plus recipient_address / target_address on the transaction row itself, exist specifically because account/mosaic history search needs to find a transaction by an address or mosaic id it never explicitly names as "the" signer. A plain column or join target is required for that to be indexable.
    Every other type-specific field, such as restrictionAdditions for an account address restriction or newRestrictionValue for a mosaic global restriction, has no such search requirement. It only needs to round-trip for detail-view rendering. Rather than adding a one-off column per field per transaction type, those fields are kept in a body JSON column: every field of the raw transaction payload except the ones already promoted to their own column, namely signer public key, max fee, deadline, size, type, and message.
  • Compute effective_fee from the block's fee multiplier using LEAST(max_fee, size * fee_multiplier), and assign a monotonic list_sequence to top-level transactions so the default transaction list can paginate without a SQL OFFSET.
  • Sync transactions as part of the existing block-sync loop, and extend rollback to remove transaction/mosaic/address rows before blocks.
  • type_name, message_type, and mosaic/address role are Postgres enum columns rather than freeform text. size is a plain integer column, not bigint, since transaction sizes fit comfortably within that range.
  • Consolidated a few small duplicated helpers, including type-label lookup, public-key-to-address derivation, timestamp conversion, and a repeated rollback delete sequence, into shared functions. Also aligned the page-size constant naming with the sibling receipts branch to avoid an unnecessary future merge conflict between the two.

Note on base branch

This PR targets explorer/backend/symbol-block-model-refactor instead of dev because it depends on that branch's block-row/format-helper extraction (puller/model/symbol/Block.py, puller/model/symbol/format.py), which hasn't merged to dev yet (see #2379 ). Once that PR merges, GitHub will offer to retarget this PR's base to dev automatically.

@daoka daoka requested review from Jaguar0625 and Wayonb July 7, 2026 08:08
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.38%. Comparing base (51481fb) to head (a12545e).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              dev    #2381      +/-   ##
==========================================
- Coverage   97.01%   95.38%   -1.63%     
==========================================
  Files         276      631     +355     
  Lines       21314    49344   +28030     
  Branches      206     1362    +1156     
==========================================
+ Hits        20678    47068   +26390     
- Misses        630     2226    +1596     
- Partials        6       50      +44     
Flag Coverage Δ
lightapi-python 98.69% <ø> (ø)
wallet-common-core 97.53% <ø> (ø)
wallet-common-ethereum 100.00% <ø> (ø)
wallet-common-symbol 96.61% <ø> (ø)
wallet-mobile-symbol 94.14% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 355 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jaguar0625 Jaguar0625 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.

👍

Comment thread explorer/puller/tests/db/test_SymbolDatabase.py Outdated
Comment thread explorer/puller/tests/db/test_SymbolDatabase.py
Comment thread explorer/puller/tests/facade/symbol/test_PullerSync.py Outdated
Comment thread explorer/puller/tests/facade/symbol/test_PullerTransactionSync.py
Comment thread explorer/puller/tests/facade/symbol/test_PullerTransactionSync.py
Comment thread explorer/puller/puller/model/symbol/Transaction.py
Comment thread explorer/puller/tests/model/symbol/test_Transaction.py Outdated
Comment thread explorer/puller/puller/model/symbol/Transaction.py Outdated
{'address': bytes.fromhex(TARGET_ADDRESS), 'role': 'target'}
])

def test_create_transaction_row_leaves_scalar_target_address_empty_for_list_target_type(self):

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.

can you explain what you're testing in this test?

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.

target_address is a scalar column only populated for transaction types with a single target (ACCOUNT_METADATA/MOSAIC_METADATA/NAMESPACE_METADATA/MOSAIC_ADDRESS_RESTRICTION). ACCOUNT_ADDRESS_RESTRICTION has a list of targets (restrictionAdditions/restrictionDeletions), which go into symbol_transaction_addresses with role='target' instead — so this test asserts the scalar column is intentionally left NULL for that type, to make sure we don't accidentally pick one arbitrary address out of the list for it.

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.

maybe rename the test or add comment so this is clearer

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.

Added a comment explaining the intent directly on the test — thanks for the suggestion.

Comment on lines +516 to +517
{'address': bytes.fromhex(TARGET_ADDRESS), 'role': 'cosignatory'},
{'address': bytes.fromhex(ALIAS_ADDRESS), 'role': 'cosignatory'}

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.

i am not sure if same role makes sense for both cosigner add/delete and when cosigner is being used as signer? @cryptoBeliever what is your opinion?

'role': 'recipient'
})

if transaction_type in (

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.

one more general comment about structure of this file intended more as informational. feel free to incorporate or not.

the way you split up the file, adding support for a transaction requires changes in multiple places (e.g. for extracting addresses and mosaics) in separate functions and additional branches.

normally, it's a good practice to minimize the number of branches (more for readability and maintenance than performance nowadays given hardware advancements). an alternative design would be to have a small class / tuple with lambdas for each transaction type. instances could live in a dict and there would be one for each transaction that would be retrieved and used for normalization.

for example, something roughly like this

TransactionHandler = namedtuple('TransactionHandler', ['extract_address', 'extract_mosaics'])

def create_transfer_handler():
        def extract_address(self, transaction):
            return [{
			'address': _bytes_from_transaction_field(transaction, 'recipientAddress'),
			'role': 'recipient'
            }]
        def extract_mosaics(self, transaction):
    		return [
		    	{
			    	'mosaic_id': mosaic['id'],
		    		'amount': int(mosaic['amount']),
		    		'role': 'transfer',
		    		'position': position
	    		}
		    	for position, mosaic in enumerate(transaction.get('mosaics', []))
		]

    return TransactionHandler(extract_address, extract_mosaics)

TRANSACTION_HANDLERS = {
    TRANSFER.value(): create_transfer_handler()
    # ...
}

again, you don't have to change to this since what you have is fine, but i just wanted to call out a different architecture.

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.

Thanks for the pointer — makes sense as the codebase grows, but I'll leave the current per-field-branch structure for this phase and revisit if it gets unwieldy.

Base automatically changed from explorer/backend/symbol-block-model-refactor to dev July 8, 2026 02:23
daoka added 13 commits July 8, 2026 11:35
…ansactions

Unconfirmed/partial transactions are permanently served from a short-TTL
node cache and never persisted to symbol_transactions (this puller only
syncs /transactions/confirmed), so group can only ever hold 'confirmed'
and has no filter index. There is no reason to keep it as a column;
the REST layer can emit "group": "confirmed" as a literal.
…and SQL cascades

- Extract camel_case_enum_name/timestamp_from_network_value/label_for_type/
  address_from_public_key into format.py, fixing actual duplication between
  Block.py and Transaction.py (identical type-label lookup, timestamp
  conversion, and public-key-to-address derivation logic)
- Consolidate the repeated DELETE cascade in SymbolDatabase.py into
  _delete_blocks_and_transactions_from_height
- Move duplicated SIGNER_PUBLIC_KEY/SIGNER_ADDRESS/RECIPIENT_ADDRESS/
  BENEFICIARY_ADDRESS test constants into tests/test/SymbolTestConstants.py
…AGE_SIZE into MAX_PAGE_SIZE

Match the naming symbol-receipts-puller already uses for the same kind
of constant, so this line doesn't conflict whichever of the two sibling
branches merges second.
@daoka daoka force-pushed the explorer/backend/symbol-transactions-puller branch from 931ec8c to 38919e7 Compare July 8, 2026 02:43

@Jaguar0625 Jaguar0625 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.

👍

@daoka daoka merged commit 557226b into dev Jul 9, 2026
33 checks passed
@daoka daoka deleted the explorer/backend/symbol-transactions-puller branch July 9, 2026 01:23
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