multi: manage Taproot Assets over RPC and wavecli - #1138
Conversation
There was a problem hiding this comment.
💡 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".
| for _, op := range existing { | ||
| if op == disclosure.Outpoint { | ||
| result.AlreadyBoarded = true | ||
|
|
||
| return result, nil |
There was a problem hiding this comment.
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 👍 / 👎.
| context.WithoutCancel(ctx), &chainsource.RegisterConfRequest{ | ||
| CallerID: "taproot-asset-board-" + | ||
| outpoint.String(), | ||
| Txid: &txid, | ||
| PkScript: pkScript, |
There was a problem hiding this comment.
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 👍 / 👎.
| if req.AssetRef != "" { | ||
| filtered = filterDescriptorsByAssetRef(filtered, req.AssetRef) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 👍 / 👎.
📚 Doc drift advisoryThis PR's Go changes leave the per-package docs for Proposed
|
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.
51270e6 to
1030a96
Compare
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.
|
Now covered end to end by |
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.
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.
BoardTaprootAssetcompletes an onboarded output's path into a round from the idempotency key alone.OnboardTaprootAssetnow 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 withalready_boarded, and an unconfirmed output returnsFailedPreconditionrather than blocking.ClaimTaprootAssetVTXOclaims 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 withIncludeBlock, 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.
GetBalancegains ataproot_assetsbreakdown per asset reference (live, pending, and exiting amounts in asset units; carrier satoshis stay counted as Bitcoin), andListVTXOsgains anasset_reffilter with tap-sdk-equivalent reference matching.CLI. The
taproot-assetssubtree growsboard,claim,list,balance, andsend. 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.