Skip to content

feat: Asset and Extension#139

Draft
louisinger wants to merge 11 commits into
masterfrom
extension
Draft

feat: Asset and Extension#139
louisinger wants to merge 11 commits into
masterfrom
extension

Conversation

@louisinger
Copy link
Copy Markdown
Contributor

@louisinger louisinger commented Apr 7, 2026

@altafan please review

Summary by CodeRabbit

Release Notes

  • New Features

    • Assets issued locally are now persisted and retrievable via new GetAssetDetails method
    • Transaction history now includes asset information in each transaction
    • Off-chain transactions now support custom extension packets through an options-based interface
  • Tests

    • Added comprehensive end-to-end tests for asset management, custom extensions, and transaction asset tracking

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds asset persistence to a local store upon issuance, introduces a variadic options pattern for SendOffChain to support custom extension packets, enriches transaction history with asset information derived from packets, and exposes a new GetAssetDetails method for local asset lookup without indexer queries.

Changes

Cohort / File(s) Summary
Asset Persistence & Lookup
asset.go, sdk.go
Added GetAssetDetails(ctx, assetId) method to retrieve asset information from local AssetStore and introduced persistIssuedAssets helper that writes AssetInfo entries post-issuance with special handling for control assets (suppressing ControlAssetId on first issued id to avoid self-loops). Updated interface signature to include new method.
Transaction Asset Derivation
client.go
Added deriveAssetsFromPacket helper to extract per-asset summaries from transaction packets, grouping/summing amounts by asset id. Updated GetTransactionHistory to populate empty Transaction.Assets fields. Modified offchain transaction creation to compute absolute deltas between input/output amounts and set TxReceived/TxSent accordingly. Extended vtxosToTxs, saveSendTransaction, and saveBatchTransaction to populate Transaction.Assets using NetVtxoAssets.
SendOffChain Options Pattern
sdk.go, send.go, send_opts.go
Updated SendOffChain signature to accept variadic SendOffChainOption. Introduced new SendOffChainOption type and WithExtension(packets) constructor for custom packet support. Implemented applySendOffChainOptions to validate and apply options, with conditional appending of extension packets to client options.
Transaction Assets Database Layer
store/sql/sqlc/queries/models.go, store/sql/sqlc/queries/query.sql.go, store/sql/sqlc/query.sql, store/sql/tx_store.go
Added assets column (TEXT) to tx table via migrations. Generated Tx.Assets sql.NullString field. Updated InsertTx, SelectAllTxs, and SelectTxs SQL operations to handle new column. Modified AddTransactions to serialize tx.Assets to JSON and rowToTx to deserialize from database.
Database Migrations
store/sql/migration/20260408120000_add_tx_assets_column.up.sql, store/sql/migration/20260408120000_add_tx_assets_column.down.sql
Added forward and rollback migrations for new assets column in tx table.
Test Fixtures & Utilities
store/service_test.go, test/e2e/utils_test.go
Extended testTxs fixture to include sample Assets field entries. Added helpers: faucetOffchainAndWait for async faucet operations with event blocking, fetchExtensionFromVirtualTx to retrieve extension envelopes from indexed virtual transactions, and findTxByID for transaction lookup by key.
End-to-End Tests
test/e2e/extension_test.go
Added three comprehensive tests: TestSendWithExtraCustomPacket validates extension packet rejection for reserved types and correct payload storage; TestGetAssetDetails verifies asset retrieval with metadata and control asset linking; TestTransactionHistoryAssets confirms asset information derivation in transaction history.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AssetStore as Local AssetStore
    participant TxStore as Transaction Store
    participant Indexer

    Client->>Client: IssueAsset(ctx, params)
    Client->>Client: Save spend transaction
    Client->>Client: persistIssuedAssets(issued_ids)
    loop For each issued asset
        Client->>AssetStore: UpsertAsset(assetId, assetInfo)
        AssetStore-->>Client: Success (log errors if occur)
    end

    Client->>Indexer: GetTransactionHistory(ctx)
    Indexer-->>Client: [Transactions with AssetPackets]
    
    loop For each transaction with empty Assets
        Client->>Client: deriveAssetsFromPacket(packet)
        Client->>Client: Group by asset id, sum amounts
        Client->>TxStore: Update Transaction.Assets
    end

    Client->>AssetStore: GetAssetDetails(ctx, assetId)
    AssetStore-->>Client: AssetInfo
Loading
sequenceDiagram
    participant Client
    participant SDK as SDK SendOffChain
    participant Options as Options Handler
    participant ARK as ARK Protocol

    Client->>SDK: SendOffChain(ctx, receivers, opts...)
    SDK->>Options: applySendOffChainOptions(opts)
    loop For each option
        Options->>Options: Validate non-nil option
        Options->>Options: Apply to sendOffChainOptions
    end
    Options-->>SDK: Error or success
    alt Option error
        SDK-->>Client: Return error immediately
    else Success
        SDK->>SDK: Build clientOpts with WithVtxos
        alt Extension packets provided
            SDK->>SDK: Append WithExtraCustomPacket
        end
        SDK->>ARK: Call ArkClient.SendOffChain(clientOpts)
        ARK-->>SDK: Transaction result
        SDK-->>Client: Return txid
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Opt out expiry sorting #69: Modifies the SendOffChain API signature with a variadic options pattern, directly related to the options handling changes in this PR.
  • Arkade Asset #58: Introduces asset persistence and GetAssetDetails method for local asset retrieval, matching the asset management features added here.
  • chore: bump arklib  #102: Modifies asset-packet handling in transaction paths, related to the asset derivation and transaction enrichment logic in this PR.

Suggested reviewers

  • altafan
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive terms that don't clearly convey the specific changes—'Asset and Extension' lacks specificity about what was actually added or modified. Consider using a more specific title that highlights the primary changes, such as 'feat: Add asset persistence and extension packet support' or similar that better describes the main objectives.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch extension

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/e2e/utils_test.go (1)

198-217: Consider making the sleep configurable or adding retry logic.

The hardcoded 2-second sleep could cause test flakiness under load or slow CI environments. For E2E tests this is often acceptable, but if flakiness occurs, consider a polling approach with timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/utils_test.go` around lines 198 - 217, Replace the hardcoded
time.Sleep in fetchExtensionFromVirtualTx with a polling/retry loop (or make the
wait duration configurable) that repeatedly calls idxr.GetVirtualTxs until the
tx appears or a timeout is reached; specifically, poll GetVirtualTxs for txid
with a short interval and overall timeout, returning an error (or failing the
test via require) if the timeout elapses before extension.NewExtensionFromTx
succeeds, so tests are resilient in slow CI or under load.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client_internal_test.go`:
- Around line 136-137: Remove the extra consecutive blank line in
client_internal_test.go that causes gofmt to fail (the blank lines around lines
136-137); open the file and delete one of the two adjacent empty lines so the
file is formatted correctly and the gofmt/golangci-lint check will pass.

In `@client.go`:
- Around line 355-366: There is trailing whitespace on the blank line inside the
block that builds the result slice (around where order, result,
clientTypes.Asset and sums are used); open the function that constructs and
returns result, remove the trailing spaces on the empty line between the
len(order) check and the result := line so the file is gofmt-compliant, then
re-run formatting/linting.

In `@sdk.go`:
- Around line 18-19: The file sdk.go has consecutive blank lines (around lines
18-19) causing gofmt to fail; remove the extra blank line so there are no
duplicate blank lines in that area, then run gofmt/goimports (or go fmt) to
ensure formatting passes CI; update the commit with the cleaned sdk.go file.

---

Nitpick comments:
In `@test/e2e/utils_test.go`:
- Around line 198-217: Replace the hardcoded time.Sleep in
fetchExtensionFromVirtualTx with a polling/retry loop (or make the wait duration
configurable) that repeatedly calls idxr.GetVirtualTxs until the tx appears or a
timeout is reached; specifically, poll GetVirtualTxs for txid with a short
interval and overall timeout, returning an error (or failing the test via
require) if the timeout elapses before extension.NewExtensionFromTx succeeds, so
tests are resilient in slow CI or under load.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 099eee11-a8de-417d-9141-aa3c0e4d05c1

📥 Commits

Reviewing files that changed from the base of the PR and between 7435d18 and fbfce09.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • asset.go
  • client.go
  • client_internal_test.go
  • go.mod
  • sdk.go
  • send.go
  • test/e2e/extension_test.go
  • test/e2e/utils_test.go

Comment thread client_internal_test.go Outdated
Comment thread client.go
Comment thread sdk.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
client_internal_test.go (2)

212-214: Helper comment is slightly inaccurate vs implementation.

Line 213-Line 214 says it constructs a “single fake non-nil AssetInput,” but mustBuildPacket creates one input per output and only for non-issuance groups. Consider updating the comment to match the actual behavior.

Suggested wording tweak
-// It constructs groups using a single fake non-nil AssetInput so that the
-// reissuance/burn invariants in asset.NewAssetGroup don't reject them.
+// For non-issuance groups, it constructs matching inputs per output so that
+// asset.NewAssetGroup input/output invariants are satisfied.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client_internal_test.go` around lines 212 - 214, Update the doc comment for
mustBuildPacket to accurately describe its behavior: explain that it assembles
an asset.Packet from group descriptions by creating one fake non-nil AssetInput
per output (not a single input), and that these fake inputs are only created for
non-issuance groups so asset.NewAssetGroup invariants aren’t rejected; reference
mustBuildPacket and the behavior around "non-issuance groups" and "one input per
output" so the comment matches the implementation.

189-203: Add a subtest for invalid txid derivation fallback.

deriveAssetsFromPacket skips groups when asset.NewAssetId(txid, groupIndex) fails (see client.go:347-353). Please add one test that passes an invalid txid and asserts the issuance-only packet yields nil, so this behavior is explicitly pinned.

Proposed test addition
 	t.Run("issuance group derives id from txid and group index", func(t *testing.T) {
 		t.Parallel()
@@
 		require.Equal(t, uint64(20), got[0].Amount)
 	})
+
+	t.Run("issuance group with invalid txid is skipped", func(t *testing.T) {
+		t.Parallel()
+		pkt := mustBuildPacket(t, []testGroup{
+			{id: nil, outs: []uint64{5}},
+		})
+		require.Nil(t, deriveAssetsFromPacket(pkt, "not-a-valid-txid"))
+	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client_internal_test.go` around lines 189 - 203, Add a subtest to
client_internal_test.go that verifies deriveAssetsFromPacket returns nil for
issuance-only packets when asset.NewAssetId fails for an invalid txid: create an
invalid txid (so asset.NewAssetId(txid, 0) will error), build an issuance-only
packet with mustBuildPacket (one group with nil id and outputs), call
deriveAssetsFromPacket(pkt, invalidTxid) and assert the result is nil/empty;
this pins the fallback behavior in deriveAssetsFromPacket and references
deriveAssetsFromPacket and asset.NewAssetId so reviewers can locate the relevant
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sdk.go`:
- Around line 51-53: The public SDK should not alias client.SendOption directly;
replace the SendOffChain signature to use a new SDK-owned type
SendOffChainOption (similar to InitOption/BatchSessionOption), remove the type
alias that exposes client.WithVtxos, and implement only SDK-supported helpers
(e.g., WithExtension) for SendOffChainOption so callers cannot pass unsupported
options like client.WithVtxos; update all call sites and the interface
declaration for SendOffChain(ctx context.Context, receivers
[]clientTypes.Receiver, opts ...SendOffChainOption) to accept the new type and
ensure send.go (where client.WithVtxos(vtxos) is prepended) still prepends vtxos
internally rather than relying on caller-provided vtxos.

---

Nitpick comments:
In `@client_internal_test.go`:
- Around line 212-214: Update the doc comment for mustBuildPacket to accurately
describe its behavior: explain that it assembles an asset.Packet from group
descriptions by creating one fake non-nil AssetInput per output (not a single
input), and that these fake inputs are only created for non-issuance groups so
asset.NewAssetGroup invariants aren’t rejected; reference mustBuildPacket and
the behavior around "non-issuance groups" and "one input per output" so the
comment matches the implementation.
- Around line 189-203: Add a subtest to client_internal_test.go that verifies
deriveAssetsFromPacket returns nil for issuance-only packets when
asset.NewAssetId fails for an invalid txid: create an invalid txid (so
asset.NewAssetId(txid, 0) will error), build an issuance-only packet with
mustBuildPacket (one group with nil id and outputs), call
deriveAssetsFromPacket(pkt, invalidTxid) and assert the result is nil/empty;
this pins the fallback behavior in deriveAssetsFromPacket and references
deriveAssetsFromPacket and asset.NewAssetId so reviewers can locate the relevant
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80f200d8-6192-46d4-8b1a-7545661baf15

📥 Commits

Reviewing files that changed from the base of the PR and between fbfce09 and 9d8ece4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • client.go
  • client_internal_test.go
  • go.mod
  • sdk.go
  • store/service_test.go
  • store/sql/migration/20260408120000_add_tx_assets_column.down.sql
  • store/sql/migration/20260408120000_add_tx_assets_column.up.sql
  • store/sql/sqlc/queries/models.go
  • store/sql/sqlc/queries/query.sql.go
  • store/sql/sqlc/query.sql
  • store/sql/tx_store.go
  • test/e2e/extension_test.go
  • test/e2e/utils_test.go
✅ Files skipped from review due to trivial changes (4)
  • store/sql/migration/20260408120000_add_tx_assets_column.up.sql
  • store/sql/migration/20260408120000_add_tx_assets_column.down.sql
  • store/sql/sqlc/queries/models.go
  • go.mod
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/e2e/extension_test.go
  • test/e2e/utils_test.go
  • client.go

Comment thread sdk.go
@louisinger
Copy link
Copy Markdown
Contributor Author

related to arkade-os/arkd#1003

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client.go`:
- Around line 317-329: The current fallback uses deriveAssetsFromPacket on
history[i].AssetPacket which sums all outputs and overstates assets when the
packet contains outputs not owned by this wallet; change the logic so the
fallback is only used when ownership is unambiguous (e.g., the packet's outputs
can be proven to belong exclusively to this wallet) or, preferably, stop relying
on this read-side reconstruction and ensure wallet-scoped Assets are populated
on write paths that persist Transaction.Assets; update the loop that touches
history[i].Assets (and any similar code between lines ~333-375) to skip
deriveAssetsFromPacket unless a clear ownership check succeeds (or add a TODO
and wire up the write-paths to set history[i].Assets during persistence) and
update any tests to cover a send-with-change case so the behavior is validated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc7cef5e-8af4-4a96-ad93-35cec82f4427

📥 Commits

Reviewing files that changed from the base of the PR and between ffe3629 and d06e07c.

📒 Files selected for processing (3)
  • client.go
  • sdk.go
  • store/service_test.go
✅ Files skipped from review due to trivial changes (2)
  • store/service_test.go
  • sdk.go

Comment thread client.go
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

🔍 Arkana Code Review — arkade-os/go-sdk#139

Reviewed the full diff plus surrounding file context. This PR adds asset persistence at issuance, transaction history asset enrichment, a variadic options pattern for SendOffChain, GetAssetDetails, and supporting DB migrations. Solid work overall — detailed findings below.


🟡 Issues to address

1. Potential uint64 underflow removed, but zero-amount edge case introduced
client.gohandleArkTx (around the old line 1465, new switch block)

The old code had inAmount - outAmount which would panic/underflow if outAmount > inAmount. The new switch-based logic fixes that — good. However, when inAmount == outAmount, the result is amount = 0 with txType = TxSent. A zero-amount "sent" transaction will show up in the user's history. Consider either:

  • Skipping the tx entirely when the delta is zero (it's a wash), or
  • Adding a comment explaining why a zero-amount entry is intentional.

2. Redundant length check in persistIssuedAssets
asset.go:76if len(assetIds) > 0 is always true because line 63 already returns early when len(assetIds) == 0. Harmless but noisy — remove it or add a comment for the reader.

3. deriveAssetsFromPacket: uint16 cast of groupIndex
client.go (new deriveAssetsFromPacket function, around line 162):

derived, err := asset.NewAssetId(txid, uint16(groupIndex))

groupIndex is an int from range pkt. If a packet ever had >65535 groups, this silently truncates. Extremely unlikely in practice, but a bounds check or a comment noting the protocol cap would harden this.

4. deriveAssetsFromPacket: uint64 addition without overflow check
client.go (line ~175):

sums[id] += out.Amount

If a malicious or buggy packet has amounts that sum past math.MaxUint64, this wraps silently. This is display-only (not protocol-critical), but could show wildly incorrect balances. A saturating add or overflow check would be defensive.

5. No upfront validation of reserved packet types in WithExtension
send_opts.go:23-26 — The docstring says type 0x00 is reserved and rejected by the underlying client, but the SDK layer doesn't validate this. The error only surfaces deep in the call stack. Consider adding an early check in WithExtension:

for _, p := range packets {
    if p.Type() == asset.PacketType {
        return fmt.Errorf("packet type 0x%02x is reserved for the asset packet", asset.PacketType)
    }
}

This gives SDK users a clear error at the call site, not buried in the protocol layer. The e2e test confirms the current behavior works, but fail-fast is better UX.


🔵 Cross-repo impact (important)

Breaking interface change: Adding GetAssetDetails and changing SendOffChain(ctx, receivers)SendOffChain(ctx, receivers, opts...) on the ArkClient interface (sdk.go) is a breaking change for any code that provides its own implementation of ArkClient.

Known consumers:

  • fulmine (internal/core/application/service.go:89, pkg/swap/swap.go:40, pkg/swap/batch_handler.go:41) — embeds the concrete arksdk.ArkClient returned by NewArkClient/LoadArkClient, so it inherits the new methods automatically. No breakage expected unless fulmine has mock implementations in tests.
  • asset-demos/golang (main.go:44,99) — uses the interface as a parameter type but only calls existing methods. SendOffChain callers with no opts are backward-compatible. No breakage unless it implements the interface.
  • ark-simulator, cli — same pattern, should be fine.

Recommendation: Coordinate the go-sdk release with downstream consumers. If any repo has test mocks implementing ArkClient, they'll need updating.


🟢 Things done well

  • Fail-safe persistence: persistIssuedAssets logs and swallows errors so a successful on-chain issuance isn't masked by a local store failure. Correct design.
  • Self-loop prevention: Clearing ControlAssetId on the first issued asset for NewControlAsset is clean and well-documented.
  • Backward-compatible enrichment: The GetTransactionHistory fallback to deriveAssetsFromPacket when Assets is empty handles legacy rows gracefully.
  • Migration: Clean up/down migration for the assets column. Nullable TEXT is correct for backward compat.
  • Test coverage: Three new e2e tests (TestSendWithExtraCustomPacket, TestGetAssetDetails, TestTransactionHistoryAssets) cover the happy paths, negative cases, and control-asset linkage. testTxs fixture updated for unit tests too.
  • Options pattern: sendOffChainOptions with nil-check and error return is idiomatic Go.

⚪ Protocol-critical assessment

This PR touches asset handling (issuance persistence, transaction history enrichment, extension packets) but does not modify VTXO signing, forfeit paths, round lifecycle, connector trees, or exit paths. The handleArkTx change to determine tx direction is the closest to protocol-adjacent code, and the new logic is strictly more correct than the old inAmount - outAmount (which could underflow).

Verdict: Not protocol-critical. No mandatory human sign-off required for this specific PR, though the interface change warrants coordination with downstream repos.


🤖 Reviewed by Arkana · #139

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

LGTM with minor suggestions.

The five items flagged (zero-amount tx edge case, redundant len check, uint16 cast, uint64 overflow, early packet-type validation) are all non-blocking — none are protocol-critical or security issues. The cross-repo interface breakage is manageable since downstream consumers embed the concrete type.

Ship it once the minor feedback is addressed or explicitly deferred. 🚢

🤖 Arkana

@louisinger louisinger marked this pull request as draft April 15, 2026 13:31
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.

1 participant