Skip to content

feat(kotlin-sdk): tx-label & asset-lock-kind DAO resolver queries - #4251

Open
bfoss765 wants to merge 2 commits into
dashpay:v4.2-devfrom
bfoss765:port/v4.1/wallet-tx-persistence
Open

feat(kotlin-sdk): tx-label & asset-lock-kind DAO resolver queries#4251
bfoss765 wants to merge 2 commits into
dashpay:v4.2-devfrom
bfoss765:port/v4.1/wallet-tx-persistence

Conversation

@bfoss765

@bfoss765 bfoss765 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Adds the two wallet-side transaction-display persistence resolver queries the Android cutover needs:

  • TransactionDao.transactionKindForDisplayTxid — tx-label (Shielded / Invitation / Unshielded) resolution
  • AssetLockDao.fundingTypeForTxid — asset-lock funding-type (identity registration / top-up / invitation) resolution

Read-only queries; reuse the existing DashDatabase v7 schema (no migration; this branch does not bump the schema version). Extracted from the QA integration line into its own PR per the feature-branch organization.

Note: the two shielded anchored-note queries the app also uses — ShieldedDao.getUnspentAnchoredNotesByWallet and minUnspentAnchoredBlockHeight — are not in this PR; they ship with the shielded work in #4204 (they are shielded-note queries and belong there). Together, #4204 + this PR provide the four DAO symbols the app requires to compile against the SDK.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added transaction lookup by both raw and explorer-formatted transaction IDs.
    • Added asset lock funding type lookup using a transaction ID.
    • Transaction IDs are validated and converted automatically between supported formats.
    • Lookups safely return no result when the ID is invalid or no matching record exists.

Read-only DAO queries backing the app's transaction-label resolver. No schema
change — every column read already exists in DashDatabase v9, so no migration
and no new exported schema are required.

TransactionDao:
  - transactionKindForTxid(wireTxid) / transactionKindForDisplayTxid(hex):
    resolve a tx's transactionTypeKind, used to label the withdraw/unshield
    case (AssetUnlock == 7), which has no asset_locks row. Adds a private
    displayHexToWireTxid() companion (explorer display hex → wire BLOB PK).

AssetLockDao:
  - fundingTypeForTxid(txidDisplayHex): the fundingTypeRaw of the asset lock
    whose outPointHex PK is prefixed by the display txid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.2.0 milestone Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@bfoss765, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dad6e727-8eb7-41c2-8048-a1b7bc00c7c0

📥 Commits

Reviewing files that changed from the base of the PR and between 715aad2 and 176f8ed.

📒 Files selected for processing (2)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt
  • packages/kotlin-sdk/sdk/src/test/kotlin/org/dashfoundation/dashsdk/persistence/TransactionLabelResolverDaoTest.kt
📝 Walkthrough

Walkthrough

The Kotlin SDK adds DAO methods to resolve transaction kinds and asset lock funding types from wire-order or display-order transaction identifiers.

Changes

Transaction lookup APIs

Layer / File(s) Summary
Transaction kind resolution
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt
TransactionDao retrieves transaction kinds by wire-order txid or display-order txid. Display identifiers must contain 64 hexadecimal characters, then convert to wire order before lookup.
Asset lock funding lookup
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt
AssetLockDao returns fundingTypeRaw for an asset lock whose outPointHex starts with the supplied transaction identifier, or null when no match exists.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: quantumexplorer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two new Kotlin SDK DAO resolver query areas: transaction labels and asset-lock kinds.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 176f8ed)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The txid display-to-wire reversal matches the persisted transaction and outpoint encodings, the raw transaction/funding discriminants are consistent with their Rust definitions, and these read-only queries require no Room migration. However, the exact head omits two DAO methods explicitly promised by the PR and required by the stated Android cutover, so this PR does not yet deliver its advertised API surface.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/ShieldedDao.kt`:
- [BLOCKING] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/ShieldedDao.kt:52-56: Add the two promised anchored-note DAO methods
  The PR description explicitly includes `getUnspentAnchoredNotesByWallet` and `minUnspentAnchoredBlockHeight` among the four symbols required for the Android cutover, but this exact head proceeds directly from `observeUnspentNotesByWallet` to `upsertNote` without defining either method. Commit `7bc8a845c67c6010e72b171a1d8f70f498220574` in still-open PR #4204 adds those methods, but it is not an ancestor of this head; the corresponding pre-commit `ShieldedDao` section is identical to this PR's base. Merging PR #4251 alone therefore does not provide the advertised API and leaves the cutover with unresolved references. Include the queries here, or explicitly declare and enforce PR #4204 as a merge prerequisite and update this PR's stated deliverables.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt:87-91: Validate txids before using them as a LIKE pattern
  Unlike `transactionKindForDisplayTxid`, this resolver does not enforce its documented 64-character hexadecimal input contract. SQLite interprets `%` and `_` in the bound value as wildcards, so malformed inputs such as `%` or 64 underscores match unrelated asset-lock rows and `LIMIT 1` returns one row's funding type rather than null. Validate and canonicalize the input in SQL, then compare the exact 64-character outpoint prefix; generated `outPointHex` values are lowercase, while `lower(:txidHex)` preserves support for uppercase canonical txids.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt:42-56: Add Room tests for the new resolver contracts
  No SDK test invokes `transactionKindForTxid`, `transactionKindForDisplayTxid`, or `fundingTypeForTxid`. Add in-memory Room coverage using a non-palindromic txid to pin the display-to-wire byte reversal, plus missing, malformed, and uppercase input cases. The asset-lock tests should also cover multiple vouts sharing a txid and define the intended behavior if their funding types differ, because the current nullable scalar query uses `LIMIT 1` and otherwise leaves that selection dependent on row scan order.

Comment on lines +87 to +91
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE outPointHex LIKE :txidHex || ':%' LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Validate txids before using them as a LIKE pattern

Unlike transactionKindForDisplayTxid, this resolver does not enforce its documented 64-character hexadecimal input contract. SQLite interprets % and _ in the bound value as wildcards, so malformed inputs such as % or 64 underscores match unrelated asset-lock rows and LIMIT 1 returns one row's funding type rather than null. Validate and canonicalize the input in SQL, then compare the exact 64-character outpoint prefix; generated outPointHex values are lowercase, while lower(:txidHex) preserves support for uppercase canonical txids.

Suggested change
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE outPointHex LIKE :txidHex || ':%' LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE length(:txidHex) = 64 " +
"AND lower(:txidHex) NOT GLOB '*[^0-9a-f]*' " +
"AND substr(outPointHex, 1, 65) = lower(:txidHex) || ':' " +
"LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?

source: ['codex']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Validate txids before using them as a LIKE pattern no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Comment on lines +42 to +56
@Query("SELECT transactionTypeKind FROM transactions WHERE txid = :txidWire LIMIT 1")
suspend fun transactionKindForTxid(txidWire: ByteArray): Int?

/**
* Convenience over [transactionKindForTxid] keyed by the explorer
* DISPLAY txid hex (64 lowercase chars, wire order reversed) the
* resolver already holds — the same display form used by
* [AssetLockDao.fundingTypeForTxid]. Reverses to wire order before the
* BLOB PK match. Returns null for malformed hex (not 64 hex chars) or
* when no such tx is stored. Not a Room query — a plain default method
* delegating to [transactionKindForTxid].
*/
suspend fun transactionKindForDisplayTxid(txidDisplayHex: String): Int? {
val wire = displayHexToWireTxid(txidDisplayHex) ?: return null
return transactionKindForTxid(wire)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Add Room tests for the new resolver contracts

No SDK test invokes transactionKindForTxid, transactionKindForDisplayTxid, or fundingTypeForTxid. Add in-memory Room coverage using a non-palindromic txid to pin the display-to-wire byte reversal, plus missing, malformed, and uppercase input cases. The asset-lock tests should also cover multiple vouts sharing a txid and define the intended behavior if their funding types differ, because the current nullable scalar query uses LIMIT 1 and otherwise leaves that selection dependent on row scan order.

source: ['codex']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Add Room tests for the new resolver contracts no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

@shumkov

shumkov commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Confirmed no migration is needed — both columns pre-exist and Room's compile-time validation accepted the queries, so that claim is verified rather than assumed.

1. fundingTypeForTxid doesn't enforce its own documented contract. AssetLockDao.kt:87-91 binds :txidHex as the LIKE pattern, so fundingTypeForTxid("%") returns some arbitrary asset lock's funding type and 64 _ chars matches every row — a silently wrong label, not injection. The KDoc's "a txid is pure hex so the pattern carries no wildcards" is a statement about callers, and there are no callers yet to hold to it. The asymmetry is the bug: the sibling transactionKindForDisplayTxid (TransactionDao.kt:54-57) does validate and return null. Cheapest fix is the same 64-hex guard displayHexToWireTxid already applies.

2. Zero tests for all three new methods. Delete the byte-reversal loop at TransactionDao.kt:139 and nothing in CI turns red — every withdraw/unshield tx would silently lose its "Asset Unlock" label on device. The harness already exists (DashDatabase.createInMemory under Robolectric, as used by WalletDeletionTest). A non-palindromic txid fixture is the important case; "ab".repeat(32) would pass even with the reversal removed.

3. The description says DashDatabase v9; this branch is at v7 (DashDatabase.kt:104, schemas stop at 7.json). The "no migration needed" conclusion is still correct, but the version claim will send reviewers looking for a schema that isn't there — and #4240 concurrently adds 8.json.

Minor: LIMIT 1 with no ORDER BY over a one-to-many (DIP-0027 allows up to 255 credit outputs, so the row returned is scan-order dependent — worth documenting that all vouts share a funding type, or making it deterministic); this is the only query in AssetLockDao not scoped by walletId; and TransactionDao.kt:129-141 re-implements the txid half of decodeOutPointHex, which is internal in the same module and reachable.

Process note: the 🔴 blocker on this PR is filed against ShieldedDao.kt, which this PR doesn't touch, and the current PR body already states those queries ship with #4204. Because it was treated as blocking, the second review stage never ran — so this diff has had only partial automated review.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

You're right about the preliminary blocker. I revalidated the exact head (715aad23) against the full PR description and #4204: the two ShieldedDao methods are explicitly out of scope here and already assigned to #4204, so they should not block this PR.

I've queued a same-SHA automated re-review with this thread context so the deferred second stage can run and supersede the incorrect CHANGES_REQUESTED review. The remaining two inline items are still valid author-facing suggestions: validate fundingTypeForTxid inputs before matching, and add focused Room coverage for the endian conversion/resolver contracts.

GitHub would not allow this account to directly dismiss the existing review, so the fresh review must supersede it.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@thepastaclaw The ShieldedDao scope blocker does not apply to this PR. I will review the current head with the PR objectives and this thread context. I will also check the remaining fundingTypeForTxid validation and resolver test coverage concerns.

🐇

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt (1)

30-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use two-space indentation in these Kotlin changes.

The added Kotlin lines use four spaces. Update them to use two spaces.

  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt#L30-L57: use two-space indentation for the resolver query and default method.
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt#L122-L141: use two-space indentation for the companion object and converter.
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt#L77-L91: use two-space indentation for the resolver documentation and query.

As per coding guidelines, "Follow repository EditorConfig settings: 2-space indentation, 4 spaces for Rust files."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt`
around lines 30 - 57, Use two-space indentation for the resolver query and
default method around transactionKindForTxid and transactionKindForDisplayTxid
in
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt:30-57;
apply the same indentation to the companion object and converter in
TransactionDao.kt:122-141 and the resolver documentation and query in
AssetLockDao.kt:77-91. Preserve all code and behavior unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt`:
- Around line 30-57: Use two-space indentation for the resolver query and
default method around transactionKindForTxid and transactionKindForDisplayTxid
in
packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt:30-57;
apply the same indentation to the companion object and converter in
TransactionDao.kt:122-141 and the resolver documentation and query in
AssetLockDao.kt:77-91. Preserve all code and behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07e300c0-df80-4178-a390-a3a36e83a7be

📥 Commits

Reviewing files that changed from the base of the PR and between ed4116b and 715aad2.

📒 Files selected for processing (2)
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt
  • packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex + Sonnet

This PR adds two read-only Room resolver queries (TransactionDao.transactionKindForDisplayTxid/transactionKindForTxid and AssetLockDao.fundingTypeForTxid) reusing existing DashDatabase columns with no migration; the display-to-wire txid reversal is correct and Room's compile-time validation accepted the queries against the actual schema. The prior blocking ShieldedDao finding is confirmed misfiled and is dropped: the PR body explicitly scopes those two anchored-note methods to #4204, and the diff never touches ShieldedDao.kt. Two suggestion-level issues persist on the exact head — fundingTypeForTxid doesn't enforce its own documented hex-only contract before using the input as a LIKE pattern, and none of the three new resolver methods have test coverage — plus a minor doc nitpick that the PR description cites schema v9 while DashDatabase.kt declares version 7. Source: reviewers gpt-5.6-sol and claude-sonnet-5 (general); verifier claude-sonnet-5.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (completed)

🟡 2 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/AssetLockDao.kt:87-91: fundingTypeForTxid doesn't enforce its own hex-only contract before using input as a LIKE pattern
  fundingTypeForTxid binds txidHex directly into `outPointHex LIKE :txidHex || ':%'` without validating it is a 64-char pure-hex string, unlike the sibling transactionKindForDisplayTxid which enforces exactly that via displayHexToWireTxid and returns null on malformed input. SQLite treats `%`/`_` in the bound value as wildcards, so `fundingTypeForTxid("%")` matches an arbitrary asset-lock row and 64 `_` chars matches every row — with `LIMIT 1` this silently returns a wrong funding type instead of null. The in-code comment's claim that "a txid is a pure-hex string ... so the LIKE pattern carries no wildcards of its own" is an assumption about callers, not an invariant this public method enforces. Separately, even once the pattern is exact-matched, `LIMIT 1` with no `ORDER BY` over a genuinely one-to-many relationship (DIP-0027 permits multiple asset-lock outputs sharing a txid) makes the returned row scan-order dependent; if all vouts for a txid always share the same funding type this is harmless, but that invariant should be documented or the query made deterministic.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt:42-56: No test coverage for the three new resolver methods
  Nothing in the repo invokes transactionKindForTxid, transactionKindForDisplayTxid, or fundingTypeForTxid. The hand-written byte-reversal in displayHexToWireTxid (TransactionDao.kt:129-141) has no pinned regression test — deleting the reversal loop entirely would not fail CI, silently mislabeling every withdraw/unshield transaction on-device. The in-memory Room harness already used by WalletDeletionTest is available for this. Add cases with a non-palindromic txid (to catch endian regressions a symmetric fixture like "ab".repeat(32) would miss), uppercase input, malformed/short hex, no-match, and — for fundingTypeForTxid — multiple asset-lock rows sharing a txid prefix to pin down the LIMIT 1 selection behavior.

Comment on lines +87 to +91
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE outPointHex LIKE :txidHex || ':%' LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: fundingTypeForTxid doesn't enforce its own hex-only contract before using input as a LIKE pattern

fundingTypeForTxid binds txidHex directly into outPointHex LIKE :txidHex || ':%' without validating it is a 64-char pure-hex string, unlike the sibling transactionKindForDisplayTxid which enforces exactly that via displayHexToWireTxid and returns null on malformed input. SQLite treats %/_ in the bound value as wildcards, so fundingTypeForTxid("%") matches an arbitrary asset-lock row and 64 _ chars matches every row — with LIMIT 1 this silently returns a wrong funding type instead of null. The in-code comment's claim that "a txid is a pure-hex string ... so the LIKE pattern carries no wildcards of its own" is an assumption about callers, not an invariant this public method enforces. Separately, even once the pattern is exact-matched, LIMIT 1 with no ORDER BY over a genuinely one-to-many relationship (DIP-0027 permits multiple asset-lock outputs sharing a txid) makes the returned row scan-order dependent; if all vouts for a txid always share the same funding type this is harmless, but that invariant should be documented or the query made deterministic.

Suggested change
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE outPointHex LIKE :txidHex || ':%' LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?
@Query(
"SELECT fundingTypeRaw FROM asset_locks " +
"WHERE length(:txidHex) = 64 " +
"AND lower(:txidHex) NOT GLOB '*[^0-9a-f]*' " +
"AND substr(outPointHex, 1, 65) = lower(:txidHex) || ':' " +
"LIMIT 1"
)
suspend fun fundingTypeForTxid(txidHex: String): Int?

source: ['claude', 'codex']

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.

Fixed in 176f8ed — adopted the suggested query shape: the SQL now enforces the 64-hex contract itself (length(:txidHex) = 64 + hex-only GLOB, lower()-canonicalized) and compares the exact 65-char <txid>: prefix via substr, so there is no LIKE pattern left for %/_ to exploit — fundingTypeForTxid("%") and 64 underscores now return null instead of an arbitrary row's funding type. The multi-vout LIMIT 1 point is addressed in the KDoc: DIP-0027 rows for one txid come from the same funding flow and share fundingTypeRaw, so the unordered pick is value-stable (and the new test pins that with two vouts sharing a txid).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 176f8edfundingTypeForTxid doesn't enforce its own hex-only contract before using input as a LIKE pattern no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Comment on lines +42 to +56
@Query("SELECT transactionTypeKind FROM transactions WHERE txid = :txidWire LIMIT 1")
suspend fun transactionKindForTxid(txidWire: ByteArray): Int?

/**
* Convenience over [transactionKindForTxid] keyed by the explorer
* DISPLAY txid hex (64 lowercase chars, wire order reversed) the
* resolver already holds — the same display form used by
* [AssetLockDao.fundingTypeForTxid]. Reverses to wire order before the
* BLOB PK match. Returns null for malformed hex (not 64 hex chars) or
* when no such tx is stored. Not a Room query — a plain default method
* delegating to [transactionKindForTxid].
*/
suspend fun transactionKindForDisplayTxid(txidDisplayHex: String): Int? {
val wire = displayHexToWireTxid(txidDisplayHex) ?: return null
return transactionKindForTxid(wire)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: No test coverage for the three new resolver methods

Nothing in the repo invokes transactionKindForTxid, transactionKindForDisplayTxid, or fundingTypeForTxid. The hand-written byte-reversal in displayHexToWireTxid (TransactionDao.kt:129-141) has no pinned regression test — deleting the reversal loop entirely would not fail CI, silently mislabeling every withdraw/unshield transaction on-device. The in-memory Room harness already used by WalletDeletionTest is available for this. Add cases with a non-palindromic txid (to catch endian regressions a symmetric fixture like "ab".repeat(32) would miss), uppercase input, malformed/short hex, no-match, and — for fundingTypeForTxid — multiple asset-lock rows sharing a txid prefix to pin down the LIMIT 1 selection behavior.

source: ['claude', 'codex']

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 in 176f8ed: TransactionLabelResolverDaoTest (in-memory Room via the same Robolectric harness as WalletDeletionTest) covers all three resolvers with a NON-palindromic txid (0x01..0x20), pinning the display→wire byte reversal — the display hex must resolve while the unreversed wire hex must miss, so deleting the reversal loop in displayHexToWireTxid now fails CI. Also covered: uppercase input, malformed/short/empty hex, no-match, wildcard inputs (%, 64 underscores) against fundingTypeForTxid, and multiple asset-lock vouts sharing a txid to pin the LIMIT 1 value-stability.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — No test coverage for the three new resolver methods no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

…lver Room tests

Review fixes:

- AssetLockDao.fundingTypeForTxid no longer binds the caller's input as a
  LIKE pattern. The query now enforces the documented 64-hex contract
  itself (length + hex-only GLOB, lower()-canonicalized) and compares the
  exact 65-char `<txid>:` outPointHex prefix, so wildcard inputs like "%"
  or 64 underscores return null instead of an arbitrary row's funding
  type — matching transactionKindForDisplayTxid's null-on-malformed
  behavior. The DIP-0027 multi-vout LIMIT 1 selection is documented as
  value-stable (same-flow rows share fundingTypeRaw).

- New TransactionLabelResolverDaoTest pins all three resolver contracts
  on the in-memory Room harness with a NON-palindromic txid (0x01..0x20),
  so the display->wire byte reversal in displayHexToWireTxid has a
  regression test: display hex resolves, the unreversed wire hex misses,
  uppercase is accepted, malformed/wildcard inputs return null, and
  multiple vouts sharing a txid stay value-stable.

Verified: :sdk:testDebugUnitTest --tests '*TransactionLabelResolverDaoTest*'
passes (KSP re-validated the rewritten query).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bfoss765

Copy link
Copy Markdown
Contributor Author

Following up on the retracted preliminary blocker: confirmed the ShieldedDao methods remain out of scope here (they belong to #4204), and the two remaining author-facing suggestions are now addressed in 176f8ed:

  1. AssetLockDao.fundingTypeForTxid enforces its 64-hex contract in-query (length + hex-only GLOB + exact substr prefix match — no LIKE), returning null for wildcard or malformed input.
  2. TransactionLabelResolverDaoTest adds in-memory Room coverage for all three resolver queries with a non-palindromic txid pinning the display→wire byte reversal, plus uppercase/malformed/wildcard/no-match/multi-vout cases. Verified green via :sdk:testDebugUnitTest.

The PR description's stale "DashDatabase v9" claim is also corrected — this branch is on schema v7 with no version bump.

Since the stale CHANGES_REQUESTED review could not be dismissed directly, requesting the queued re-review run against this head and supersede it.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@bfoss765 I will review the current PR head. I will verify the reported validation, Room test coverage, and schema-version correction. I will treat the ShieldedDao scope blocker as out of scope for this PR.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex + Sonnet

This cumulative review (base ed4116b..176f8ed, latest delta 715aad2..176f8ed) confirms all three prior findings are fixed at exact head 176f8ed: AssetLockDao.fundingTypeForTxid now enforces its 64-hex/ASCII-only contract via length() + NOT GLOB '*[^0-9a-f]*' and an exact substr(outPointHex,1,65) comparison instead of a raw LIKE pattern (verified directly against the SQL); the new TransactionLabelResolverDaoTest.kt exercises all three resolver methods with a genuinely non-palindromic fixture that pins the display↔wire byte reversal and covers wildcard/malformed/uppercase/multi-vout cases; and the PR description now correctly states the queries reuse DashDatabase v7, matching DashDatabase.kt's version = 7. One new, delta-independent (present since 715aad2 but newly surfaced by Codex) low-severity finding remains: TransactionDao.displayHexToWireTxid's use of Character.digit(char, 16) accepts non-ASCII Unicode digit/letter forms (e.g. fullwidth Latin, Arabic-Indic digits) in addition to ASCII hex, which is inconsistent with the new AssetLockDao ASCII-only GLOB guard — this mirrors a pre-existing, intentionally-copied pattern from decodeOutPointHex in PlatformWalletPersistenceHandler.kt, so a proper fix should consolidate both parsers rather than patch only the new copy. No blocking issues found. Source: reviewers gpt-5.6-sol and claude-sonnet-5 (general); verifier claude-sonnet-5.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (completed)

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/dao/TransactionDao.kt:129-141: displayHexToWireTxid accepts non-ASCII Unicode hex digits via Character.digit
  Java's `Character.digit(char, 16)` is documented to accept Unicode decimal digits (any script where `isDigit()` is true and the decimal value fits the radix, e.g. Arabic-Indic `١`) and fullwidth Latin letters (`A`-`F`, `a`-`f`) in addition to ASCII `0-9a-fA-F`. A 64-character string built from these non-ASCII equivalents therefore decodes successfully instead of returning null, contradicting the method's documented contract ("Returns null for malformed hex (not 64 hex chars)") and diverging from `AssetLockDao.fundingTypeForTxid`'s new ASCII-only `NOT GLOB '*[^0-9a-f]*'` guard. Practical exposure is limited today since no caller yet exists in the tree, but this is new SDK-public surface. Note this exact `Character.digit` pattern is intentionally mirrored from the pre-existing `decodeOutPointHex` in `PlatformWalletPersistenceHandler.kt` (which the doc comment explicitly cites), so patching only this copy would leave an inconsistent sibling; the two hex-parsing loops (already flagged separately as duplicated) should be consolidated into one ASCII-only helper shared by both.

Comment on lines +129 to +141
private fun displayHexToWireTxid(displayHex: String): ByteArray? {
if (displayHex.length != 64) return null
val display = ByteArray(32)
for (i in 0 until 32) {
val hi = Character.digit(displayHex[i * 2], 16)
val lo = Character.digit(displayHex[i * 2 + 1], 16)
if (hi < 0 || lo < 0) return null
display[i] = ((hi shl 4) or lo).toByte()
}
val wire = ByteArray(32)
for (i in 0 until 32) wire[i] = display[31 - i]
return wire
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: displayHexToWireTxid accepts non-ASCII Unicode hex digits via Character.digit

Java's Character.digit(char, 16) is documented to accept Unicode decimal digits (any script where isDigit() is true and the decimal value fits the radix, e.g. Arabic-Indic ١) and fullwidth Latin letters (-, -) in addition to ASCII 0-9a-fA-F. A 64-character string built from these non-ASCII equivalents therefore decodes successfully instead of returning null, contradicting the method's documented contract ("Returns null for malformed hex (not 64 hex chars)") and diverging from AssetLockDao.fundingTypeForTxid's new ASCII-only NOT GLOB '*[^0-9a-f]*' guard. Practical exposure is limited today since no caller yet exists in the tree, but this is new SDK-public surface. Note this exact Character.digit pattern is intentionally mirrored from the pre-existing decodeOutPointHex in PlatformWalletPersistenceHandler.kt (which the doc comment explicitly cites), so patching only this copy would leave an inconsistent sibling; the two hex-parsing loops (already flagged separately as duplicated) should be consolidated into one ASCII-only helper shared by both.

Suggested change
private fun displayHexToWireTxid(displayHex: String): ByteArray? {
if (displayHex.length != 64) return null
val display = ByteArray(32)
for (i in 0 until 32) {
val hi = Character.digit(displayHex[i * 2], 16)
val lo = Character.digit(displayHex[i * 2 + 1], 16)
if (hi < 0 || lo < 0) return null
display[i] = ((hi shl 4) or lo).toByte()
}
val wire = ByteArray(32)
for (i in 0 until 32) wire[i] = display[31 - i]
return wire
}
private fun displayHexToWireTxid(displayHex: String): ByteArray? {
if (displayHex.length != 64) return null
val display = ByteArray(32)
for (i in 0 until 32) {
val hi = asciiHexDigit(displayHex[i * 2]) ?: return null
val lo = asciiHexDigit(displayHex[i * 2 + 1]) ?: return null
display[i] = ((hi shl 4) or lo).toByte()
}
val wire = ByteArray(32)
for (i in 0 until 32) wire[i] = display[31 - i]
return wire
}
private fun asciiHexDigit(char: Char): Int? = when (char) {
in '0'..'9' -> char - '0'
in 'a'..'f' -> char - 'a' + 10
in 'A'..'F' -> char - 'A' + 10
else -> null
}

source: ['claude', 'codex']

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.

3 participants