Skip to content

multi: manage Taproot Assets over RPC and wavecli - #1138

Draft
darioAnongba wants to merge 16 commits into
darioAnongba/taproot-assets-oor-runtimefrom
darioAnongba/asset-management-api
Draft

multi: manage Taproot Assets over RPC and wavecli#1138
darioAnongba wants to merge 16 commits into
darioAnongba/taproot-assets-oor-runtimefrom
darioAnongba/asset-management-api

Conversation

@darioAnongba

Copy link
Copy Markdown
Collaborator

Asset management API for the client daemon, stacked on #1062 and tracked by the integration epic; design: https://taproot-assets-ark.lightning.wiki/#api.

The PoC drove asset flows through test hooks: boarding took hand-fed confirmation transactions and exported proofs, and claiming took a hand-built map of raw blocks. This PR promotes the whole lifecycle to first-class RPCs and CLI commands, with the rule that everything the daemon can derive itself stays off the wire.

BoardTaprootAsset completes an onboarded output's path into a round from the idempotency key alone. OnboardTaprootAsset now persists the request's replay slice next to the onboarder's journal, so the board call rebuilds the disclosure, resolves the confirmation through the chain-source actor, exports the boarded proof from the daemon's own tapd (the composed output is not tapd wallet inventory, so the proof is located through the transfer that created it), and registers the boarding together with a matching asset VTXO request. Replays are safe: an existing boarding intent short-circuits with already_boarded, and an unconfirmed output returns FailedPrecondition rather than blocking.

ClaimTaprootAssetVTXO claims a matured exited leaf with daemon-gathered lineage confirmations. The lineage txids come from the leaf's own sealed package (each proof-path step names its anchor), and the chain source resolves each to its block with IncludeBlock, so the caller passes an outpoint and, optionally, a fee. A zero fee estimates one from the shared LND wallet at a conservative claim size.

Balance and listing. GetBalance gains a taproot_assets breakdown per asset reference (live, pending, and exiting amounts in asset units; carrier satoshis stay counted as Bitcoin), and ListVTXOs gains an asset_ref filter with tap-sdk-equivalent reference matching.

CLI. The taproot-assets subtree grows board, claim, list, balance, and send. The send wrapper resolves the input leaf and, for a partial send, the Bitcoin VTXO that carries the asset change, so a transfer takes only the asset, the amount, the recipient key, and an idempotency key.

The new RPCs carry the same macaroon entity as OnboardTaprootAsset (onchain:write) and are wired through the REST gateway and the REST client.

Verified by the full unit suite, the new aggregation and filter unit tests, and the CLI schema-parity tests. End-to-end exercise arrives with the regtest manual-testing harness this PR exists to serve.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51270e68dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +155 to +159
for _, op := range existing {
if op == disclosure.Outpoint {
result.AlreadyBoarded = true

return result, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not treat a persisted boarding row as completion

If RegisterAssetBoarding persists its boarding intent but its actor Await fails, or if the subsequent RegisterAssetVTXORequest fails, a retry finds that persisted outpoint here and reports success without completing the missing actor registration. A caller deadline between those two operations is enough to leave the confirmed asset output permanently unable to enter a round, so completion needs a durable marker covering both registrations rather than the boarding-store row alone.

Useful? React with 👍 / 👎.

Comment on lines +235 to +239
context.WithoutCancel(ctx), &chainsource.RegisterConfRequest{
CallerID: "taproot-asset-board-" +
outpoint.String(),
Txid: &txid,
PkScript: pkScript,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unregister each temporary confirmation watch

When an onboarded output remains unconfirmed past the 30-second wait, this detached future-mode registration survives the RPC return because no matching UnregisterConfRequest is sent; every normal polling retry therefore adds another chainsource sub-actor and backend notifier for the same output. lineageConfirmation has the same missing cleanup after successful or cancelled claim lookups, so both helpers should unregister their temporary watches on every exit path.

AGENTS.md reference: AGENTS.md:L77-L81

Useful? React with 👍 / 👎.

Comment thread waved/rpc_server.go
Comment on lines +1311 to +1312
if req.AssetRef != "" {
filtered = filterDescriptorsByAssetRef(filtered, req.AssetRef)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the asset filter before returning pending rounds

For ListVTXOs requests combining status_filter=PENDING_ROUND with a non-empty asset_ref, the earlier pending-round branch returns before reaching this filter, and listPendingRoundVTXOs applies only the minimum-amount filter. Such a request therefore returns every pending Bitcoin and asset VTXO instead of only the requested asset, which also makes taproot-assets list --status VTXO_STATUS_PENDING_ROUND --asset-ref ... misleading.

Useful? React with 👍 / 👎.

}

intent := &waverpc.TaprootAssetOORIntent{
AssetRef: assetRef,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Send the selected VTXO's canonical asset reference

When the user supplies an asset reference equivalent to, but textually different from, the stored reference, ListVTXOs successfully selects the input because filterDescriptorsByAssetRef uses tap-sdk equivalence, but this copies the user's spelling into the intent. The OOR preparer later requires exact equality with VTXO.TaprootAssetRef, so the send fails after successful selection; populate this field from input.GetTaprootAsset().GetAssetRef() instead.

Useful? React with 👍 / 👎.

@litbot-9000

Copy link
Copy Markdown
Collaborator

📚 Doc drift advisory

This PR's Go changes leave the per-package docs for waved, waverpc, and cmd/wavecli/waveclicommands stale (the new BoardTaprootAsset / ClaimTaprootAssetVTXO surface, the taproot_assets balance breakdown, the ListVTXOs.asset_ref filter, and the five new taproot-assets CLI verbs); rpc/restclient and cmd/wavecli/waveclicommands/devrpc were also in scope but are still accurate.

Proposed CLAUDE.md/AGENTS.md changes
diff --git a/cmd/wavecli/waveclicommands/AGENTS.md b/cmd/wavecli/waveclicommands/AGENTS.md
index 63b28752..720898fe 100644
--- a/cmd/wavecli/waveclicommands/AGENTS.md
+++ b/cmd/wavecli/waveclicommands/AGENTS.md
@@ -91,9 +91,19 @@ the default `--help` (revealed with `WAVELENGTH_DEV=1`) but always runnable.
 
 ### `taproot-assets.*` prototype commands
 
-The `taproot-assets onboard` command reads a complete proof file and invokes
-the durable `waverpc.OnboardTaprootAsset` workflow. It remains in the advanced
-group while the tapd/tap-sdk integration is evaluated.
+The `taproot-assets` subtree drives one asset anchor through its full
+Wavelength lifecycle: onboard → board → send/list/balance → claim after a
+unilateral exit. It remains in the advanced group while the tapd/tap-sdk
+integration is evaluated.
+
+| Command | RPC | Description |
+|---------|-----|-------------|
+| `taproot-assets onboard` | `OnboardTaprootAsset` | Read a complete proof file and move one isolated asset anchor into a standard VTXO policy. Idempotent on `--idempotency-key` |
+| `taproot-assets board` | `BoardTaprootAsset` | Complete a confirmed onboarded output's path into the next round. Takes only `--idempotency-key` (the same key `onboard` used); the daemon rebuilds the disclosure, confirmation, and boarded proof itself. Rerunnable — a replay reports `already_boarded` |
+| `taproot-assets claim` | `ClaimTaprootAssetVTXO` | Spend an exited asset VTXO's matured exit path into a fresh tapd-owned anchor. `--outpoint` required; `--fee-sat 0` (the default) asks the daemon to estimate the fee |
+| `taproot-assets list` | `ListVTXOs` | List asset-bearing VTXOs. `--asset-ref` pushes the filter into the daemon; `--status` maps a `VTXOStatus` enum name |
+| `taproot-assets balance` | `GetBalance` | Render only the `taproot_assets` section of the daemon balance, in asset units |
+| `taproot-assets send` | `ListVTXOs` + `SendOOR` | Send asset units out of round to a recipient receive pubkey. Requires `--asset-ref`, `--amount`, `--recipient-pubkey`, `--idempotency-key`; `--outpoint` pins the input VTXO |
 
 ## Key Helpers
 
@@ -193,6 +203,17 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/cmd/wave
   degrades to a "still charged the seal-time fee" warning and its
   total is absent on the wire (explicit proto presence) — it never
   blocks the flow and is never rendered as a zero fee.
+- `taproot-assets list` never reports plain Bitcoin VTXOs. With
+  `--asset-ref` the daemon applies `ListVTXOsRequest.asset_ref`; without
+  one the command still drops every row lacking a `taproot_asset` field
+  locally, because the unfiltered `ListVTXOs` response is the Bitcoin
+  listing (`ark vtxos list`).
+- `taproot-assets send` resolves its own inputs before calling `SendOOR`:
+  without `--outpoint` it picks the *smallest* live VTXO of the asset that
+  covers `--amount`, and a partial send (amount below the input's full
+  holding) additionally selects the *largest* live Bitcoin VTXO as the
+  asset-change carrier. A send with no eligible carrier fails locally
+  rather than submitting an intent the daemon would reject.
 
 ## Deep Docs
 
diff --git a/cmd/wavecli/waveclicommands/CLAUDE.md b/cmd/wavecli/waveclicommands/CLAUDE.md
index 63b28752..720898fe 100644
--- a/cmd/wavecli/waveclicommands/CLAUDE.md
+++ b/cmd/wavecli/waveclicommands/CLAUDE.md
@@ -91,9 +91,19 @@ the default `--help` (revealed with `WAVELENGTH_DEV=1`) but always runnable.
 
 ### `taproot-assets.*` prototype commands
 
-The `taproot-assets onboard` command reads a complete proof file and invokes
-the durable `waverpc.OnboardTaprootAsset` workflow. It remains in the advanced
-group while the tapd/tap-sdk integration is evaluated.
+The `taproot-assets` subtree drives one asset anchor through its full
+Wavelength lifecycle: onboard → board → send/list/balance → claim after a
+unilateral exit. It remains in the advanced group while the tapd/tap-sdk
+integration is evaluated.
+
+| Command | RPC | Description |
+|---------|-----|-------------|
+| `taproot-assets onboard` | `OnboardTaprootAsset` | Read a complete proof file and move one isolated asset anchor into a standard VTXO policy. Idempotent on `--idempotency-key` |
+| `taproot-assets board` | `BoardTaprootAsset` | Complete a confirmed onboarded output's path into the next round. Takes only `--idempotency-key` (the same key `onboard` used); the daemon rebuilds the disclosure, confirmation, and boarded proof itself. Rerunnable — a replay reports `already_boarded` |
+| `taproot-assets claim` | `ClaimTaprootAssetVTXO` | Spend an exited asset VTXO's matured exit path into a fresh tapd-owned anchor. `--outpoint` required; `--fee-sat 0` (the default) asks the daemon to estimate the fee |
+| `taproot-assets list` | `ListVTXOs` | List asset-bearing VTXOs. `--asset-ref` pushes the filter into the daemon; `--status` maps a `VTXOStatus` enum name |
+| `taproot-assets balance` | `GetBalance` | Render only the `taproot_assets` section of the daemon balance, in asset units |
+| `taproot-assets send` | `ListVTXOs` + `SendOOR` | Send asset units out of round to a recipient receive pubkey. Requires `--asset-ref`, `--amount`, `--recipient-pubkey`, `--idempotency-key`; `--outpoint` pins the input VTXO |
 
 ## Key Helpers
 
@@ -193,6 +203,17 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/cmd/wave
   degrades to a "still charged the seal-time fee" warning and its
   total is absent on the wire (explicit proto presence) — it never
   blocks the flow and is never rendered as a zero fee.
+- `taproot-assets list` never reports plain Bitcoin VTXOs. With
+  `--asset-ref` the daemon applies `ListVTXOsRequest.asset_ref`; without
+  one the command still drops every row lacking a `taproot_asset` field
+  locally, because the unfiltered `ListVTXOs` response is the Bitcoin
+  listing (`ark vtxos list`).
+- `taproot-assets send` resolves its own inputs before calling `SendOOR`:
+  without `--outpoint` it picks the *smallest* live VTXO of the asset that
+  covers `--amount`, and a partial send (amount below the input's full
+  holding) additionally selects the *largest* live Bitcoin VTXO as the
+  asset-change carrier. A send with no eligible carrier fails locally
+  rather than submitting an intent the daemon would reject.
 
 ## Deep Docs
 
diff --git a/waved/AGENTS.md b/waved/AGENTS.md
index 5a340df5..df590cd1 100644
--- a/waved/AGENTS.md
+++ b/waved/AGENTS.md
@@ -28,13 +28,19 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
 - `WalletState` — `None` / `Locked` / `Ready` wallet lifecycle.
 - `UnrollConfig` / `OORConfig` — subsystem tunables; see `Config.Validate()`
   for the invariants each enforces.
+- `BoardTaprootAssetResult` — outcome of `Server.BoardTaprootAsset`: the
+  boarded composed outpoint, its carrier Bitcoin value, the asset
+  reference/amount, and `AlreadyBoarded` marking an idempotent replay.
+- `ErrAssetBoardingUnconfirmed` — sentinel for an onboarded output that has
+  not confirmed yet; `RPCServer.BoardTaprootAsset` maps it to
+  `FailedPrecondition` so the caller retries after the next block.
 
 ## Relationships
 
 - **Depends on**: `baselib/actor`, `btcwbackend`, `chainbackends`,
   `chainsource`, `lib/actormsg`, `db`, `ledger`, `round`, `txconfirm`,
   `unroll`, `vtxo`, `wallet`, `walletcore`, `oor`, `serverconn`, `indexer`,
-  `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`,
+  `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`, `tapassets`,
   `vhtlcrecovery`, `vhtlcrecovery/coordinator`, `vhtlcrecovery/unrollpolicy`.
 - **Depended on by**: `cmd/waved`.
 
@@ -157,6 +163,42 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   exceeds `OORConfig.MaxTransientSubmitRetry` (default 1h), persisting the
   window start (`FirstRejectUnixNanos`) in the outgoing snapshot (version 5)
   so the bound survives restarts.
+- Taproot Asset boarding is a two-call flow. `OnboardTaprootAsset` persists
+  the caller-owned slice of the onboarding request into the shared
+  `tapassets.Store` under the `board-request/` key prefix (namespaced away
+  from the onboarder's own state under the bare request ID), so a later
+  `BoardTaprootAsset` can rebuild the disclosure from the idempotency key
+  alone. The operator key and exit delay are deliberately *not* persisted —
+  they are re-derived from live operator terms on every replay.
+- `BoardTaprootAsset` is idempotent through the boarding-intent set, not a
+  stored flag: it scans `FetchBoardingIntentOutpoints` for the disclosure's
+  outpoint and returns `AlreadyBoarded` rather than handing the round actor
+  a duplicate intent. Only after that check does it register the boarding
+  plus its matching asset VTXO request.
+- The two asset chain waits are bounded in opposite ways on purpose. The
+  onboarded output's confirmation wait is capped at `assetBoardingConfWait`
+  (30s) and degrades to `ErrAssetBoardingUnconfirmed` instead of pinning the
+  RPC until the next block, while the claim path's lineage confirmations are
+  unbounded — those transactions are already confirmed, so the await is
+  bounded by the historical-dispatch rescan. Both register with the chain
+  source under `context.WithoutCancel`, so a client disconnect cannot cancel
+  the registration mid-flight.
+- `ClaimTaprootAssetVTXO` with `fee_sat = 0` estimates from the LND
+  `WalletKit` estimator over `assetClaimVsizeEstimate` (200 vbytes). The
+  other wallet backends have no estimator seam here, so they must pass
+  `fee_sat` explicitly rather than silently getting a guessed fee.
+- `GetBalance` reports asset holdings in **asset units** under
+  `taproot_assets`; the carrier satoshis of those VTXOs stay counted in the
+  Bitcoin fields, so the two must never be summed. `taprootAssetBalances`
+  emits entries ordered by asset reference so repeated calls diff cleanly.
+- `ListVTXOs.asset_ref` matching goes through `tapsdk.ParseAssetRef` and
+  `Equivalent`, so an issuance-ID form matches its stored canonical form; an
+  unparseable reference degrades to exact string comparison instead of
+  erroring. Bitcoin-only VTXOs never match.
+- `BoardTaprootAsset` and `ClaimTaprootAssetVTXO` are granted under
+  `entityOnChain:"write"` alongside `OnboardTaprootAsset` in
+  `newWavedRPCPermissions`; a new asset RPC must be added there or the
+  macaroon bakery rejects it.
 - `operatorTermsFromResponse` and daemon `GetInfo` must preserve
   `FreeRefreshWindowBlocks` end to end.
 - The VTXO manager reads `FreeRefreshWindowBlocks` from the latest cached
diff --git a/waved/CLAUDE.md b/waved/CLAUDE.md
index 5a340df5..df590cd1 100644
--- a/waved/CLAUDE.md
+++ b/waved/CLAUDE.md
@@ -28,13 +28,19 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
 - `WalletState` — `None` / `Locked` / `Ready` wallet lifecycle.
 - `UnrollConfig` / `OORConfig` — subsystem tunables; see `Config.Validate()`
   for the invariants each enforces.
+- `BoardTaprootAssetResult` — outcome of `Server.BoardTaprootAsset`: the
+  boarded composed outpoint, its carrier Bitcoin value, the asset
+  reference/amount, and `AlreadyBoarded` marking an idempotent replay.
+- `ErrAssetBoardingUnconfirmed` — sentinel for an onboarded output that has
+  not confirmed yet; `RPCServer.BoardTaprootAsset` maps it to
+  `FailedPrecondition` so the caller retries after the next block.
 
 ## Relationships
 
 - **Depends on**: `baselib/actor`, `btcwbackend`, `chainbackends`,
   `chainsource`, `lib/actormsg`, `db`, `ledger`, `round`, `txconfirm`,
   `unroll`, `vtxo`, `wallet`, `walletcore`, `oor`, `serverconn`, `indexer`,
-  `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`,
+  `arkrpc`, `lndbackend`, `fraud`, `gateway`, `rpc/restclient`, `tapassets`,
   `vhtlcrecovery`, `vhtlcrecovery/coordinator`, `vhtlcrecovery/unrollpolicy`.
 - **Depended on by**: `cmd/waved`.
 
@@ -157,6 +163,42 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   exceeds `OORConfig.MaxTransientSubmitRetry` (default 1h), persisting the
   window start (`FirstRejectUnixNanos`) in the outgoing snapshot (version 5)
   so the bound survives restarts.
+- Taproot Asset boarding is a two-call flow. `OnboardTaprootAsset` persists
+  the caller-owned slice of the onboarding request into the shared
+  `tapassets.Store` under the `board-request/` key prefix (namespaced away
+  from the onboarder's own state under the bare request ID), so a later
+  `BoardTaprootAsset` can rebuild the disclosure from the idempotency key
+  alone. The operator key and exit delay are deliberately *not* persisted —
+  they are re-derived from live operator terms on every replay.
+- `BoardTaprootAsset` is idempotent through the boarding-intent set, not a
+  stored flag: it scans `FetchBoardingIntentOutpoints` for the disclosure's
+  outpoint and returns `AlreadyBoarded` rather than handing the round actor
+  a duplicate intent. Only after that check does it register the boarding
+  plus its matching asset VTXO request.
+- The two asset chain waits are bounded in opposite ways on purpose. The
+  onboarded output's confirmation wait is capped at `assetBoardingConfWait`
+  (30s) and degrades to `ErrAssetBoardingUnconfirmed` instead of pinning the
+  RPC until the next block, while the claim path's lineage confirmations are
+  unbounded — those transactions are already confirmed, so the await is
+  bounded by the historical-dispatch rescan. Both register with the chain
+  source under `context.WithoutCancel`, so a client disconnect cannot cancel
+  the registration mid-flight.
+- `ClaimTaprootAssetVTXO` with `fee_sat = 0` estimates from the LND
+  `WalletKit` estimator over `assetClaimVsizeEstimate` (200 vbytes). The
+  other wallet backends have no estimator seam here, so they must pass
+  `fee_sat` explicitly rather than silently getting a guessed fee.
+- `GetBalance` reports asset holdings in **asset units** under
+  `taproot_assets`; the carrier satoshis of those VTXOs stay counted in the
+  Bitcoin fields, so the two must never be summed. `taprootAssetBalances`
+  emits entries ordered by asset reference so repeated calls diff cleanly.
+- `ListVTXOs.asset_ref` matching goes through `tapsdk.ParseAssetRef` and
+  `Equivalent`, so an issuance-ID form matches its stored canonical form; an
+  unparseable reference degrades to exact string comparison instead of
+  erroring. Bitcoin-only VTXOs never match.
+- `BoardTaprootAsset` and `ClaimTaprootAssetVTXO` are granted under
+  `entityOnChain:"write"` alongside `OnboardTaprootAsset` in
+  `newWavedRPCPermissions`; a new asset RPC must be added there or the
+  macaroon bakery rejects it.
 - `operatorTermsFromResponse` and daemon `GetInfo` must preserve
   `FreeRefreshWindowBlocks` end to end.
 - The VTXO manager reads `FreeRefreshWindowBlocks` from the latest cached
diff --git a/waverpc/AGENTS.md b/waverpc/AGENTS.md
index 608abb8e..356fb93f 100644
--- a/waverpc/AGENTS.md
+++ b/waverpc/AGENTS.md
@@ -2,8 +2,9 @@
 
 ## Purpose
 
-Daemon gRPC API definitions for wallet, boarding, round, OOR, unroll, and
-VHTLC-recovery operations. Proto source: `waverpc/daemon.proto`. Generated
+Daemon gRPC API definitions for wallet, boarding, round, OOR, unroll,
+Taproot Asset, and VHTLC-recovery operations. Proto source:
+`waverpc/daemon.proto`; REST-gateway route map: `waverpc/daemon.yaml`. Generated
 gRPC, REST-gateway, and mailbox-RPC stubs plus one hand-written helper file
 (`errors.go`) for structured wallet-lifecycle errors.
 
@@ -36,6 +37,9 @@ gRPC, REST-gateway, and mailbox-RPC stubs plus one hand-written helper file
 - **Never edit generated code** (`daemon.pb.go`, `daemon_grpc.pb.go`,
   `daemon.pb.gw.go`, `daemon_mailboxrpc.pb.go`) — regenerate via `make rpc`
   after editing `daemon.proto` or `daemon.yaml`.
+- A new `DaemonService` method needs a matching `http` selector in
+  `daemon.yaml` or it never reaches the REST gateway, and the hand-written
+  `rpc/restclient` client for that method has no path to post to.
 - `errors.go` is hand-written and not regenerated; callers must match wallet
   lifecycle errors via `IsWalletNotReadyError`/`WalletNotReadyState`, never by
   parsing the error message string.
diff --git a/waverpc/CLAUDE.md b/waverpc/CLAUDE.md
index 608abb8e..356fb93f 100644
--- a/waverpc/CLAUDE.md
+++ b/waverpc/CLAUDE.md
@@ -2,8 +2,9 @@
 
 ## Purpose
 
-Daemon gRPC API definitions for wallet, boarding, round, OOR, unroll, and
-VHTLC-recovery operations. Proto source: `waverpc/daemon.proto`. Generated
+Daemon gRPC API definitions for wallet, boarding, round, OOR, unroll,
+Taproot Asset, and VHTLC-recovery operations. Proto source:
+`waverpc/daemon.proto`; REST-gateway route map: `waverpc/daemon.yaml`. Generated
 gRPC, REST-gateway, and mailbox-RPC stubs plus one hand-written helper file
 (`errors.go`) for structured wallet-lifecycle errors.
 
@@ -36,6 +37,9 @@ gRPC, REST-gateway, and mailbox-RPC stubs plus one hand-written helper file
 - **Never edit generated code** (`daemon.pb.go`, `daemon_grpc.pb.go`,
   `daemon.pb.gw.go`, `daemon_mailboxrpc.pb.go`) — regenerate via `make rpc`
   after editing `daemon.proto` or `daemon.yaml`.
+- A new `DaemonService` method needs a matching `http` selector in
+  `daemon.yaml` or it never reaches the REST gateway, and the hand-written
+  `rpc/restclient` client for that method has no path to post to.
 - `errors.go` is hand-written and not regenerated; callers must match wallet
   lifecycle errors via `IsWalletNotReadyError`/`WalletNotReadyState`, never by
   parsing the error message string.

How to apply

Save the diff above to a file and git apply it from the repo root, or run
the doc-gardening skill locally over just these packages:

/doc-gardening waved
/doc-gardening waverpc
/doc-gardening cmd/wavecli/waveclicommands

Notes

  • rpc/restclient and cmd/wavecli/waveclicommands/devrpc were in scope
    (both have Go changes and a CLAUDE.md) but need no edit: the two new REST
    methods and the regenerated registry_generated.go entries are already
    covered by those docs' existing generic descriptions.
  • make doc-check reports 4 pre-existing CLAUDE.md/AGENTS.md divergences
    (db, lib/tree, rpc/roundpb, vtxo) that this PR did not cause and
    this advisory does not touch — they are the nightly full sweep's job. The
    three pairs proposed above are byte-identical.

Advisory only — this check never fails the build. Run: https://github.com/lightninglabs/wavelength/actions/runs/31603093512

@darioAnongba darioAnongba self-assigned this Aug 12, 2026
@darioAnongba
darioAnongba marked this pull request as draft August 12, 2026 16:22
BoardTaprootAsset completes an onboarded output's path into a round
from the idempotency key alone, and ClaimTaprootAssetVTXO claims an
exited leaf with daemon-gathered lineage confirmations. GetBalance
gains a per-asset breakdown and ListVTXOs an asset filter.
The onboarded output's script key is not part of tapd's wallet
inventory, so its confirmed proof file is located through the
transfer that created it.
Boarding completes from the idempotency key: the daemon persists the
onboarding request, replays the disclosure, resolves the confirmation
through the chain source, exports the boarded proof from its own
tapd, and registers the boarding with a matching asset VTXO request.
Claims gather the leaf's lineage confirmations from the sealed
package's proof path, so callers stop hand-feeding raw blocks. The
balance response aggregates asset holdings by reference and the VTXO
listing filters on one.
The taproot-assets subtree grows board, claim, list, balance, and
send. The send wrapper selects the input VTXO and the asset-change
carrier from the daemon's own listing, so a transfer takes only the
asset, amount, and recipient key.
@darioAnongba
darioAnongba force-pushed the darioAnongba/asset-management-api branch from 51270e6 to 1030a96 Compare August 12, 2026 16:41
OnboardTaprootAsset built the composed boarding output under the
VTXO exit delay, while AssetBoardingDisclosure replays the same
onboarding under the boarding exit delay. The replay digest never
matched, so every BoardTaprootAsset failed, and the output itself
carried a delay round admission would reject.
The LND chain backend refuses a confirmation registration without a
height hint or an output script. Persist the onboarding-time height
in the boarding replay slice and hand it to the boarding watch, and
resolve each claim lineage watch's script from the VTXO's ancestry.
@darioAnongba

Copy link
Copy Markdown
Collaborator Author

Now covered end to end by TestRoundAssetManagementAPI in lumos#731, which drives the full lifecycle through these RPCs alone. The first live run caught two bugs, fixed here: the onboarding ran under the VTXO exit delay while the boarding disclosure replays under the boarding exit delay, so the idempotency digest never matched (e45ac96); and LND refuses confirmation watches without a height hint and an output script, so the onboarding height is persisted in the replay slice and claim lineage watches resolve each anchor's script from the VTXO's own ancestry (3c4ac4b).

An empty input_proof_file now lets OnboardTaprootAsset export the
proof of the wallet's own matching UTXO from tapd, so callers no
longer hand-export it with tapcli. The wallet must hold the amount
in exactly one UTXO; an explicit proof file still works unchanged.
The Taproot Asset subtree now registers as "assets" with
"taproot-assets" kept as an alias, so existing scripts keep
working while the everyday name gets shorter.
A batch transition consumed its funding sources exactly, so a round
could only be funded from a UTXO holding the batched amount. An
optional change output now returns the surplus to the operator's own
tapd wallet, where it stays ordinary spendable inventory. The wallet
keys are derived once and pinned on the request: the split commitment
binds the change script key and every anchor position, and derivation
and commit must build identical transitions.
A partial asset send no longer names a whole Bitcoin VTXO as its
change carrier. Zero now defers to the operator minimum, and the
selection remainder returns to the sender as plain Bitcoin change.
A partial asset send used to consume a whole Bitcoin VTXO as the
asset-change carrier. The daemon now defaults an omitted carrier to
the operator minimum and returns the input remainder as a self-owned
plain Bitcoin change output; a sub-floor remainder folds into the
asset-change carrier instead of becoming a dust output. wavecli
passes the default instead of selecting a full VTXO.
An incoming receive session is driven by a single recipient event, and
only that event's output carried the policy and asset overlay, so the
second self-addressed output of a partial asset send never
materialized. The composed asset-change script is now registered as an
owned alias at send time and asset outputs without an overlay take
their identity from the indexer metadata, authenticated by the
composed pkScript check.
Onboarding no longer needs one UTXO holding exactly the requested
amount. Say so on the request: the amount is what boards, the funding
UTXOs only have to cover it, and an explicit proof selects one of
them rather than all of the units.

Comment-only regeneration; the wire format is unchanged.
Onboarding consumed exactly one tapd UTXO whose amount equalled the
requested amount, so a wallet holding its units across several anchors
could not board at all. Select the daemon's own unleased UTXOs until
they cover the amount, spend each as its own asset input, and return
the surplus to that same wallet on a second asset output. The change
keys are derived once and pinned in the durable state, because the
split commitment binds the change script key and every rebuild has to
reproduce the identical transition. Exact funding still commits
today's single-output transition, and the request digest now takes the
funding proofs in content order so a replay of the persisted set
rebuilds the same request.
An asset boarding no longer needs fresh Bitcoin: it charges its round's
fee to Bitcoin the client already holds in Ark. That consumes a coin the
caller never named, so the response reports which one and what it was
worth. The change returns as that value minus the operator's seal-time
fee.
An asset VTXO request is FixedAmount, so a round carrying only one gives
the operator no output to stamp the seal-time residual on and its quote
rejects the whole intent. That is why boarding an asset has needed a
simultaneous Bitcoin boarding, and therefore a fresh on-chain deposit
every time.

BoardTaprootAsset now refreshes one live Bitcoin VTXO into the same
assembling round instead. The forfeit adds input value, the refresh
output is the non-fixed slot the residual lands on, and the remainder
comes back as change. Selection is smallest-sufficient over the quoted
fee plus the operator's minimum VTXO amount, so a large coin is never
churned for a small fee.

The check runs before the boarding is persisted, so a wallet with no
spendable Bitcoin fails with FailedPrecondition naming the fix rather
than assembling a round the operator rejects at seal. It is skipped when
the client's intents for that round already own a non-fixed output,
which leaves the same-round boarding flow untouched.
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