Skip to content

Feat/delegate contract#156

Open
bitcoin-coder-bob wants to merge 9 commits into
feat/contract-watcherfrom
feat/delegate-contract
Open

Feat/delegate contract#156
bitcoin-coder-bob wants to merge 9 commits into
feat/contract-watcherfrom
feat/delegate-contract

Conversation

@bitcoin-coder-bob
Copy link
Copy Markdown
Contributor

@bitcoin-coder-bob bitcoin-coder-bob commented Apr 24, 2026

closes #149

Summary

  • Add DelegateHandler deriving an offchain VTXO contract with a 3-of-3 (owner, delegate, server) spending path alongside the standard forfeit and unilateral-exit paths
  • Add Manager.NewDelegate with dedup, concurrent-safe check-then-create, and contract_created event emission
  • Expand watcher test coverage from 6 to 16 tests; fix latent >= 0 readiness check in TestWatcher_StopClosesEvents

Review feedback addressed

  • Tapscript ordering (P0): add TestDelegateHandler_ClosureOrdering, which asserts via ForfeitClosures() that the 2-of-2 forfeit is at index 0 and the 3-of-3 delegate is at index 1 — swapping them breaks both the address assertion and the new closure-order assertion
  • Delegate key encoding (P1): switch ParamDelegateKey storage and the NewDelegate dedup lookup from x-only schnorr (32 bytes) to SerializeCompressed (33 bytes), preventing a theoretical collision between keys with the same x-coordinate but opposite y-parity

Summary by CodeRabbit

  • New Features

    • Added support for creating and managing delegate contracts within the Ark protocol.
    • Delegate contracts now support both unilateral and collaborative spending paths with configurable exit delays.
  • Tests

    • Comprehensive test coverage added for delegate contract derivation, validation, and spending path selection.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ff4bfcc-212c-4985-8544-e2c5f8acf616

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces delegate contract support to the Go SDK by adding a DelegateHandler that derives Ark delegate VTXO contracts with owner-signer-delegate authorization, constructs three tapscripts in fixed index order, and provides contract parameter extraction and spend path routing for unilateral and collaborative scenarios, along with comprehensive test coverage and manager integration.

Changes

Delegate Contract Implementation

Layer / File(s) Summary
Type Definitions & Constants
types/types.go, contract/delegate_handler.go
Added ContractTypeDelegate constant and introduced DelegateConfig, PathContext, PathSelection structs plus DelegateHandler type.
Contract Handler Core
contract/delegate_handler.go
DeriveContract validates delegate key against nil and equality constraints, constructs exit/forfeit/delegate tapscripts in fixed index order, derives taproot key/address, and encodes parameters into a types.Contract.
Contract Parameter Extraction & Parsing
contract/delegate_handler.go
Implemented GetKeyRef, GetKeyRefs, GetSignerKey, GetExitDelay, GetTapscripts to extract and decode contract parameters; added helpers for serializing/parsing tapscripts and delay strings.
Spend Path Selection & Routing
contract/delegate_handler.go
SelectPath routes to a single tapscript leaf based on collaborative/unilateral flags and sequence/locktime context; GetSpendablePaths enumerates all valid spend paths with appropriate metadata.
Manager Integration
contract/manager.go, contract/types.go
Added client field to manager, updated GetHandler to return DelegateHandler for delegate contracts, implemented NewDelegate method to fetch server info, compute exit delay, derive and store delegate contracts, and emit results.
Tests & Mock Updates
contract/delegate_handler_test.go, contract/watcher_test.go, spendable_vtxos_test.go
Added comprehensive test suite validating contract derivation, determinism, parameter extraction, and spend path logic; updated mock implementations to satisfy extended Manager interface.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat/delegate contract' is concise and clearly identifies the main feature addition (delegate contract support), which aligns with the primary objective and substantial code changes across multiple files.
Linked Issues check ✅ Passed The PR implements all required coding components from #149: DelegateHandler for deriving and interpreting delegate contracts, Manager.NewDelegate for creation, test coverage across handler/manager/watcher, and proper spending path support with owner, delegate, and signer keys.
Out of Scope Changes check ✅ Passed All changes align with issue #149 scope: delegate contract handler implementation, manager integration, type definitions, and comprehensive test coverage. Minor mock updates in watcher and spendable_vtxos tests are necessary integration adjustments, not out-of-scope additions.

✏️ 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 feat/delegate-contract

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.

@bitcoin-coder-bob bitcoin-coder-bob changed the base branch from master to feat/contract-watcher April 24, 2026 19:13
@bitcoin-coder-bob bitcoin-coder-bob marked this pull request as ready for review April 24, 2026 19:23
@bitcoin-coder-bob
Copy link
Copy Markdown
Contributor Author

@arkanaai @coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

@bitcoin-coder-bob Sure, I'll review the changes in this PR now!


🧠 Learnings used
Learnt from: bitcoin-coder-bob
Repo: arkade-os/go-sdk PR: 0
File: :0-0
Timestamp: 2026-04-22T16:50:05.830Z
Learning: In arkade-os/go-sdk PR `#153` (contract watcher), the following MEDIUM findings were explicitly deferred to follow-up PRs by maintainer bitcoin-coder-bob: `#7` TOCTOU in Load() (functionally safe due to upsert), `#9` SELECT * in contract store (fragile on schema changes), `#10` contract reload per ark tx event (O(n) per event, consider caching), `#11` missing tests for new public API methods.

Learnt from: bitcoin-coder-bob
Repo: arkade-os/go-sdk PR: 0
File: :0-0
Timestamp: 2026-04-22T02:29:29.878Z
Learning: In arkade-os/go-sdk PR `#145`, maintainer altafan recommended scoping the ContractManager PR down to only: contract types, Manager interface, DefaultHandler, and client integration wiring. DelegateHandler, VHTLCHandler, Registry (DefaultRegistry global), and Watcher should be deferred to follow-up PRs to reduce breaking changes and speed up merge.

Learnt from: sekulicd
Repo: arkade-os/go-sdk PR: 145
File: contract/handlers/register.go:5-5
Timestamp: 2026-04-21T13:27:50.964Z
Learning: In arkade-os/go-sdk, the use of `init()` functions for registering contract handlers (contract/handlers/register.go) and the `DefaultRegistry` global (contract/registry.go) are considered anti-patterns by maintainer sekulicd. The preferred approach is explicit registration via a `WithContractHandlers(...contract.Handler)` functional option in `NewArkClient`, constructing a local registry and passing it to `contract.NewManager`. The `DefaultRegistry` package-level global should be removed entirely.
✅ Actions performed

Review triggered.

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.

@arkanaai
Copy link
Copy Markdown

arkanaai Bot commented Apr 24, 2026

Arkana Code Review — feat/delegate-contract

Thanks @bitcoin-coder-bob. Clean implementation overall — the tapscript structure is sound and the tests are solid. A few things worth discussing before this merges:


⚠️ Protocol-Critical Flag

This PR touches VTXO tapscript construction and forfeit paths, so flagging it as protocol-critical. A human must sign off before merge regardless of automated review approval.


Design Questions

1. SelectPath has no way to select the delegate leaf

PathContext only carries Collaborative bool. In collaborative mode, SelectPath always returns the forfeit leaf (index 1). There's currently no way for a caller to explicitly select the delegate leaf (index 2) via SelectPath — they'd have to call GetSpendablePaths and pick index 2 manually.

If a wallet holding the delegate key needs to construct a spend using that path, they have no clean way to do it through the Manager interface. Consider adding a Delegated bool field to PathContext, or a separate SelectDelegatePath method. If this is intentional (delegate path is always selected out-of-band), that should be documented explicitly.

2. Key ordering in the delegate leaf

The delegate leaf is constructed as [owner, delegate, server]:

&script.MultisigClosure{
    PubKeys: []*btcec.PublicKey{key.PubKey, delegateKey, cfg.SignerPubKey},
},

For OP_CHECKSIGADD-based multisig, witness ordering must match script key ordering. Is [owner, delegate, server] the canonical order agreed upon with the server/ASP? If the server expects a different ordering when co-signing the delegate path, witness construction will fail silently (invalid sig, not a parse error). Worth confirming this is aligned with the ASP's expectation.

3. Key leak on persist failure in NewDelegate

key, err := m.ks.NewKey(ctx)  // key is created and committed to the keystore
// ...
if err := m.persistAndCache(ctx, *c); err != nil {
    return nil, err             // key exists but contract is lost
}

If persistAndCache fails, the wallet key has been generated but the contract is not stored — there's no rollback. The same pattern exists in the default handler, so this may be an accepted trade-off, but it's worth noting. A compensating mechanism (e.g. re-deriving delegate contracts from stored keys on startup) would be needed to recover from this.


Minor Observations

  • GetSpendablePaths non-collaborative correctly returns only exit — good.
  • No boarding/onchain facets — explicitly documented, fine.
  • persistAndCache + emit are the same pattern as NewKey in the default handler — consistent.
  • Tests cover determinism, key isolation, path selection, error paths — well done.

Summary

The core tapscript construction and contract lifecycle look correct. The main open question is the SelectPath gap for the delegate leaf — either PathContext needs a Delegated field, or the design intent (delegate spending is always caller-directed via GetSpendablePaths) needs to be documented. Key ordering alignment with the ASP should also be confirmed.

Needs human sign-off before merge (protocol-critical).

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.

Code Review — feat/delegate-contract

Reviewed the full diff, all changed files in context, the underlying arkd script package, and cross-repo consumers. This is protocol-critical code (VTXO contract construction and spending paths). Requesting changes + human review.


🔴 P0 — Forfeit Closure Classification Is Ordering-Dependent

delegate_handler.go:47-49 — The delegate leaf MultisigClosure{PubKeys: [owner, delegate, server]} will be classified as a forfeit closure by ForfeitClosures() in arkd/pkg/ark-lib/script/vtxo_script.go:204-213, because ForfeitClosures() matches ALL *MultisigClosure types regardless of key count.

The client-lib (batch_session_handler.go:653,857, send.go:397, utils.go:425) consistently uses forfeitClosures[0], which works only because the 2-of-2 forfeit at leaf [1] appears before the 3-of-3 delegate at leaf [2] in the closures slice.

This ordering is load-bearing and undocumented. If anyone reorders the closures (forfeit at [2], delegate at [1]), the client-lib will pick the 3-of-3 delegate leaf as the forfeit path. The round will fail silently because only 2 of 3 signatures are available — or worse, succeed partially and create an unspendable forfeit transaction.

Recommendations:

  1. Add a prominent doc comment on the closure ordering requirement in DelegateHandler.DeriveContract, explaining WHY the order matters.
  2. Consider adding a dedicated closure type (e.g., DelegateMultisigClosure) in arkd so ForfeitClosures() can distinguish delegate paths from actual forfeit paths. File as a follow-up issue at minimum.
  3. Add a test that verifies ForfeitClosures() on the produced script returns exactly 1 forfeit (not 2).

🔴 P1 — Nil delegateKey Panics

delegate_handler.go:34DeriveContract does not validate that delegateKey is non-nil. A nil pointer passed here will panic at schnorr.SerializePubKey(delegateKey) (line 49, inside the MultisigClosure PubKeys) or at the Params serialization (line 87).

// Add at the top of DeriveContract:
if delegateKey == nil {
    return nil, fmt.Errorf("delegate key must not be nil")
}

Same for manager.go:271NewDelegate passes delegateKey straight through without validation.


🟡 P2 — No Way to Select the Delegate Path

delegate_handler.go:92-107SelectPath returns forfeit (collaborative) or exit (non-collaborative). The delegate 3-of-3 path (leaf [2]) is never selectable through SelectPath. It only appears in GetSpendablePaths.

How does the delegate actually initiate a spend? If the intent is that delegate spending goes through a different code path, document it. If SelectPath should support it, PathContext needs a field (e.g., UseDelegatePath bool) to select leaf [2].


🟡 P3 — No Key Identity Validation

delegate_handler.go:42-49 — No validation that delegateKey differs from key.PubKey (owner) or cfg.SignerPubKey (server).

  • If delegateKey == ownerKey: the 3-of-3 becomes effectively 2-of-3 since one party signs twice. The security guarantee of requiring a distinct third party is silently bypassed.
  • If delegateKey == signerKey: same issue, server controls 2-of-3.

Add explicit checks:

if delegateKey.IsEqual(key.PubKey) {
    return nil, fmt.Errorf("delegate key must differ from owner key")
}
if delegateKey.IsEqual(cfg.SignerPubKey) {
    return nil, fmt.Errorf("delegate key must differ from signer key")
}

🟢 P4 — Minor / Style

  1. manager.go:296-324 — The SelectPath / GetSpendablePaths type switches will need manual updates for every new contract type. Consider a handler registry (map[string]Handler). Not blocking.

  2. Test coverage is solid. delegate_handler_test.go covers derive, determinism, different-delegate-different-script, path selection (both directions), spendable paths, and error cases. Good.

  3. Mock updates in watcher_test.go:131-146 and spendable_vtxos_test.go:113-128 — Correct and necessary for the interface expansion. No issues.


Cross-Repo Impact

  • No downstream breakage. The contract package is not imported by any consumer repo (bancod, demos, asset-demos). They depend on higher-level arksdk.ArkClient.
  • No related open PRs found on consumer repos.
  • arkd server compatibility: ParseVtxoScript handles arbitrary leaf counts. Validate() passes because the delegate closure contains the signer key. The forfeitClosures[0] usage happens to pick the correct leaf due to ordering (see P0 above).

Verdict

The implementation is clean, well-tested, and follows existing patterns. However, the forfeit closure classification issue (P0) is a latent correctness risk that needs documentation at minimum and ideally a structural fix in arkd. The nil panic (P1) is a straightforward fix. P2 and P3 should be addressed before this ships.

⚠️ Protocol-critical: requires human sign-off before merge.

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 Protocol Review — Delegate Contract Handler

Verdict: Request changes (protocol-critical — requires human sign-off)

This PR adds a delegate spending path (3-of-3: owner + delegate + server) to Ark VTXOs. Any change to VTXO script structure is protocol-critical. The implementation is clean and well-tested, but I have one concrete issue and protocol-level flags that need human eyes.


🚨 Issue: Missing UseDelegatePath in PathContext (current base)

The diff adds UseDelegatePath bool to PathContext (types.go) and DelegateHandler.SelectPath uses it to choose between the 2-of-2 forfeit leaf and the 3-of-3 delegate leaf. However:

delegate_handler.go:106-108SelectPath returns the delegate leaf (tapscripts[2]) when pctx.UseDelegatePath is true, but there is no validation that UseDelegatePath is only set for delegate-type contracts. If a caller sets UseDelegatePath: true on a TypeDefault contract, DefaultHandler.SelectPath silently ignores it and returns the forfeit leaf. This is technically safe but semantically confusing — consider returning an error from manager.go:SelectPath if UseDelegatePath is set on a non-delegate contract type, or document the no-op behavior explicitly.


⚠️ Protocol-Critical Flags (require human review)

1. Tapscript leaf ordering is load-bearing — delegate_handler.go:18-24

The comment correctly warns that closure order matters: ForfeitClosures() in arkd matches all *MultisigClosure and the 2-of-2 forfeit at index [1] must precede the 3-of-3 delegate at index [2]. The forfeit path test (delegate_handler_test.go:152-161) verifies SelectPath(Collaborative: true) returns tapscripts[1]. Good, but fragile. Consider adding an explicit test that decodes the tapscript at index [1] and asserts it contains exactly 2 pubkeys, not 3 — this would catch an accidental reorder at the script level.

2. Forfeit transaction compatibility — delegate_handler.go:53-57

The 2-of-2 forfeit closure MultisigClosure{[owner, server]} matches the default contract's forfeit path. Confirm with human reviewers that arkd's round lifecycle and forfeit-building code handles delegate VTXOs identically to default VTXOs when taking the forfeit path. If arkd's forfeit builder uses ForfeitClosures()[0] as stated in the comment, this is correct.

3. Exit path — delegate_handler.go:49-52

The unilateral exit path is CSVMultisigClosure{[owner], UnilateralExitDelay} — only the owner can exit after the CSV timelock. The delegate has no unilateral exit path, meaning a delegate cannot independently recover funds. This is the correct security property (owner retains full custody), but human reviewers should confirm this matches the intended delegation model.


✅ What looks correct

  • manager.go:280-340NewDelegate with delegateCreateMu: Properly serializes the check-create sequence. The dedup check (GetContracts → scan for matching delegate key hex) happens BEFORE m.ks.NewKey(ctx), avoiding wasted key derivation. Good.

  • manager.go:110-120 — Load skip for delegate-only keys: Correctly prevents Load() from creating default contracts (offchain/boarding/onchain) for keys that were created by NewDelegate. The logic handles the edge case of a key with both delegate and partial default contracts (proceeds to fill in missing defaults). Sound.

  • manager.go:296-320 — SelectPath/GetSpendablePaths routing: Adding TypeDefaultOnchain to the switch gives a better error path ("requires at least 2 tapscripts") instead of "unsupported type". The test at manager_test.go:309-322 explicitly verifies this. Good UX improvement.

  • delegate_handler.go:36-46 — Input validation: Nil check, owner-key check, signer-key check. All correct. Defense in depth with the manager's own nil check.

  • Test coverage: Comprehensive — derivation determinism, different-delegate-different-script, path selection for all 3 modes (collaborative, delegate, unilateral), validation errors, tapscript count boundary. The TestManager_NewDelegate tests cover dedup and isolation.


📝 Nits (non-blocking)

  1. delegate_handler.go:55cfg.SignerPubKey is used directly as the server key in the forfeit and delegate closures. If the signer key rotates between rounds, delegate contracts derived with the old key become un-forfeitable. This is the same as default contracts, but worth a comment since delegate contracts may be longer-lived.

  2. manager.go:270-272 — The nil check on delegateKey is duplicated between managerImpl.NewDelegate and DelegateHandler.DeriveContract. Defense in depth is fine; just noting it's intentional.


Bottom line: Code quality is high, patterns are consistent with DefaultHandler, tests are thorough. The one actionable issue is the UseDelegatePath semantic gap. The protocol-critical flags above need a human to confirm the delegation model matches arkd's round/forfeit behavior. Requesting changes to gate on human sign-off.

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 — feat/delegate-contract

Verdict: Request changes (protocol-critical — requires human sign-off)

This PR adds a delegate VTXO contract type with a 3-of-3 (owner, delegate, server) spending path alongside the standard exit and forfeit paths. The code is well-structured, mirrors DefaultHandler correctly, and has strong test coverage including concurrency. Three items need attention before merge.


🔴 P0 — Tapscript ordering is load-bearing and fragile

contract/delegate_handler.go:36-39 — The comment correctly warns:

The 2-of-2 forfeit MUST remain at index [1] so it is picked ahead of the 3-of-3 delegate at index [2]. Do not reorder.

I verified this against arkd source (pkg/ark-lib/script/vtxo_script.go:204-213): ForfeitClosures() matches all *MultisigClosure regardless of key count and returns them in closure-list order. Downstream consumers (e.g. bancod/pkg/contract/taker.go:253) use forfeitClosures[0]. If the 3-of-3 delegate closure were to appear before the 2-of-2 forfeit, the server would attempt to build forfeit transactions requiring a delegate signature it cannot obtain — effectively making the VTXO unforfeitable.

The ordering is correct today, but this is a footgun. Recommendation: add a unit test in delegate_handler_test.go that explicitly asserts the closure type and key count at each index after TapTree() construction, so any reordering breaks the build, not production.

🟡 P1 — NewDelegate dedup check uses schnorr.SerializePubKey for key comparison

contract/manager.go (diff hunk at +280-356) — The deduplication logic serializes delegate keys with schnorr.SerializePubKey (x-only, 32 bytes) and compares hex strings:

delegateKeyHex := hex.EncodeToString(schnorr.SerializePubKey(delegateKey))
// ...
if existing[i].Params[ParamDelegateKey] == delegateKeyHex {

Two distinct secp256k1 public keys with the same x-coordinate but different y-parity will collide here, causing NewDelegate to return the wrong contract. While btcec.PublicKey typically normalises to even-y (so this is unlikely in practice), the comparison should use the full 33-byte compressed encoding to be safe, or at minimum document the assumption that only even-y keys are accepted.

This same pattern exists in DeriveContract (delegate_handler.go:108) where the param is stored. Fix in both places if changing.

🟢 P2 — Delegate key ordering in 3-of-3 MultisigClosure

contract/delegate_handler.go:72-74 — The delegate closure is constructed as:

PubKeys: []*btcec.PublicKey{key.PubKey, delegateKey, cfg.SignerPubKey},

This key ordering (owner, delegate, server) determines the signing order in the MuSig/multisig script. Please confirm this matches the server-side expectation in arkd. If arkd expects (owner, server, delegate) or any other ordering, signature verification will fail silently — the tap leaf hash won't match and the spend path will be unusable.


✅ What looks good

  • Concurrency: delegateCreateMu correctly serializes the check-then-create sequence; lock ordering (delegateCreateMumu.RLock) is consistent with defaultCreateMu usage. Concurrent test (TestManager_NewDelegate_Concurrent) with 20 goroutines validates this.
  • Load() skip logic (manager.go diff hunk at +110-125): Correctly prevents Load() from creating default contracts for delegate-only keys, while still handling crash recovery for partial NewDefault writes.
  • SelectPath dispatch (manager.go:296-306): Adding TypeDefaultOnchain to the default handler case is a deliberate choice — the handler returns a descriptive "tapscripts" error rather than "unsupported type". Tested.
  • Validation: nil check, owner-key collision, signer-key collision all covered.
  • Test coverage: Comprehensive — derivation, determinism, different-delegate-different-script, path selection (all 4 combinations of collaborative×delegate), spendable paths, event emission/non-emission on reuse, concurrent access, keystore errors.
  • No cross-repo breakage: No downstream Go consumers directly implement the Manager interface outside this repo. Mock updates in watcher_test.go and spendable_vtxos_test.go are correct.

⚠️ Protocol-critical: This PR modifies VTXO contract construction and spending paths. Even though the code looks correct, human sign-off is required before merge per protocol-critical review policy.

🤖 Reviewed by Arkana

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 — #156

Verdict: Request changes (minor) + request human review (protocol-critical)

This PR adds a DelegateHandler that derives offchain VTXO contracts with a 3-of-3 delegate spending path (owner + delegate + server) alongside the standard 2-of-2 forfeit and CSV exit paths. It also extends Manager with NewDelegate, SelectPath, and GetSpendablePaths, and hardens the Watcher with new tests.


🔴 Protocol-Critical — Requires Human Review

This PR modifies VTXO tapscript structure, which is protocol-critical. Even though the code looks correct after thorough analysis, a human must sign off before merge.

Closure ordering is load-bearing and correct. I verified against the upstream ForfeitClosures() implementation in arkd/pkg/ark-lib/script/vtxo_script.go:204-213:

  • ForfeitClosures() matches *MultisigClosure, *CLTVMultisigClosure, *ConditionMultisigClosure
  • It does NOT match *CSVMultisigClosure (exit closure)
  • With the delegate layout [0]=CSV exit, [1]=2-of-2 forfeit, [2]=3-of-3 delegate:
    • ForfeitClosures()[0] = 2-of-2 (owner+server) ✓
    • ForfeitClosures()[1] = 3-of-3 (owner+delegate+server) ✓
  • Downstream consumer bancod/pkg/contract/taker.go:253 uses forfeitClosures[0] for forfeit signing — this correctly picks the 2-of-2 path even for delegate VTXOs.

Security property confirmed: The server can forfeit a delegate VTXO via the 2-of-2 path without the delegate's participation. This is correct — delegates must not have veto power over forfeit (anti-double-spend) operations.

The TestDelegateHandler_ClosureOrdering test at delegate_handler_test.go:515-575 is excellent — it independently reconstructs the closure layout and verifies ForfeitClosures() returns the expected order. This is the kind of test that prevents future reordering regressions.


🟡 Bug: Doc/code mismatch on ParamDelegateKey serialization

contract/types.go — The ParamDelegateKey constant comment says:

ParamDelegateKey = "delegateKey" // hex-encoded schnorr delegate public key

But the actual serialization in delegate_handler.go:108 and manager.go (NewDelegate) uses SerializeCompressed() (33-byte with 02/03 parity prefix), not schnorr.SerializePubKey() (32-byte x-only):

ParamDelegateKey: hex.EncodeToString(delegateKey.SerializeCompressed()),

This is internally consistent (storage and lookup both use compressed), so it's not a functional bug. But the comment will mislead future developers who try to deserialize with schnorr.DeserializePubKey().

Fix: Change the comment to // hex-encoded compressed delegate public key.


✅ What looks good

  1. Validation (delegate_handler.go:46-54): nil check, delegate≠owner, delegate≠signer — prevents degenerate contracts.

  2. Concurrency (manager.go): delegateCreateMu serializes the check-then-create in NewDelegate, preventing duplicate contracts for the same delegate key under concurrent calls. Verified by TestManager_NewDelegate_Concurrent with 20 goroutines.

  3. Load() skip logic (manager.go:109-118): Correctly skips default contract derivation for keys that only have delegate contracts, preventing unwanted side effects during wallet reload.

  4. SelectPath routing (manager.go): Adding TypeDefaultOnchain to the switch intentionally routes it through DefaultHandler which fails with "requires at least 2 tapscripts" — a more informative error than "unsupported type". Test at manager_test.go confirms this.

  5. Watcher tests: Backoff retry, context cancellation, address dedup, unsubscribe-on-stop, concurrent stop, malformed address handling — all solid additions that harden the watcher against real-world failure modes.

  6. Test coverage: Comprehensive across all new code paths — derivation, path selection, manager integration, concurrency, error propagation, and the critical closure ordering invariant.

  7. Cross-repo impact: Minimal. No external repos directly import go-sdk/contract. The Manager interface expansion is SDK-internal. PathContext.UseDelegatePath is additive.


Summary

The protocol logic is sound and well-tested. One doc fix needed on ParamDelegateKey. Human review required on the VTXO tapscript structure change per protocol-critical policy.

🤖 Reviewed by Arkana

Resolves conflicts from feat/contract-watcher rebase. The contractManager
architecture (types.Contract, handlers.Handler interface, GetHandler/NewContract/
ScanContracts API) replaces the old managerImpl. DelegateHandler is ported to the
new interface: DeriveContract now returns *types.Contract using DelegateConfig
instead of *clientTypes.Config, and the handler implements handlers.Handler with
pre-computed tapscripts and "block:N"/"second:N" exit delay format stored in params.
Manager.NewDelegate is added to support delegate contract creation with a delegate key.
@bitcoin-coder-bob
Copy link
Copy Markdown
Contributor Author

@coderabbitai @arkanaai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

@bitcoin-coder-bob Sure, I'll review the latest changes in this PR!

✅ Actions performed

Review triggered.

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.

@arkanaai
Copy link
Copy Markdown

arkanaai Bot commented May 8, 2026

Arkana Code Review — Feat/delegate contract

⚠️ Protocol-critical flag: This PR touches VTXO tapscript structure, forfeit paths, and CSV exit paths. Per our review policy, human sign-off is required before merge regardless of the review outcome below.


✅ Previous feedback addressed

Both prior requests are correctly resolved:

  • P0 — Tapscript ordering: TestDelegateHandler_ClosureOrdering correctly pins the invariant. It directly calls vtxoScript.ForfeitClosures() and asserts [0] is 2-of-2 (owner+server) and [1] is 3-of-3 (owner+delegate+server), then cross-checks by comparing the tap key from DeriveContract against the manually-built closure layout. Swapping the closures breaks both assertions. Solid.

  • P1 — Delegate key encoding: ParamDelegateKey is now stored as SerializeCompressed() (33 bytes), preventing the theoretical x-coordinate collision. The dedup lookup will correctly distinguish keys with identical x but opposite y-parity.


🔴 New concerns

1. Exit-delay type inference — potential CSV mismatch (manager.go, NewDelegate)

```go
if delay < 512 {
exitDelay = arklib.RelativeLocktime{Type: arklib.LocktimeTypeBlock, Value: uint32(delay)}
}
```

The < 512 threshold works correctly if info.UnilateralExitDelay is always denominated in seconds (BIP68 time-relative granules are 512 s each, so < 512 must be blocks). But if the server ever returns the value in blocks directly (e.g. 144 blocks), this would encode 144 as a block locktime — correct by accident but for wrong reasons. The other handlers likely solve this identically — if so, this is consistent and fine — but a comment citing the BIP68 floor, or confirmation of the server's unit, would harden it.

2. Manual unlocks in NewDelegate — defer risk (manager.go)

Every error branch in NewDelegate calls m.mu.Unlock() manually — currently six early-return paths. Any future error path added without a matching unlock produces a deadlock. The standard Go pattern:

```go
var created *types.Contract
func() {
m.mu.Lock()
defer m.mu.Unlock()
// all store+key work, assign created
}()
if created != nil {
m.emit(*created)
}
return created, nil
```

Not a correctness bug today, but this pattern has caused real deadlocks in similar codebases and is worth fixing.

3. No dedup by delegateKey for sequential calls

Concurrent calls are serialised by m.mu — good. But two sequential calls with the same delegateKey will create two contracts with different owner keys. The PR description says "dedup" is included. If dedup is intentionally only for concurrent races, that's fine but worth a clarifying comment. If the intent is "one contract per delegateKey", there should be a check comparing ParamDelegateKey in existing contracts before deriving a new one.


🟡 Minor notes

  • Doc comment (delegate_handler.go:72): "The 2-of-2 forfeit MUST remain at index [1]" would read more clearly as "must be forfeitClosures[0], which corresponds to Closures[1]" — the current phrasing is slightly ambiguous about which index system it's referring to.

  • No GetDelegateKey helper: Callers wishing to verify or display the delegate key must decode c.Params[ParamDelegateKey] themselves. A small helper alongside GetSignerKey and GetExitDelay would round out the API.


Summary

The core logic is sound — the 3-of-3 tapscript structure is correct, the closure ordering invariant is well-tested, and the 33-byte delegate key encoding is the right fix. The two things I'd want resolved before merge are: (1) confirming the exit-delay unit assumption or adding a comment, and (2) clarifying/implementing the delegateKey dedup semantics. The manual unlock pattern is low-risk now but worth addressing.

🚩 Requires explicit human approval — protocol-critical VTXO/forfeit path change.

🤖 Reviewed by Arkana

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contract/manager.go (1)

51-54: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Delegate contracts are still invisible to manager-wide discovery.

Because ContractTypeDelegate is not registered in m.handlers, GetSupportedContractTypes() will omit it and ScanContracts() will never consider it. The later GetHandler special-case only fixes direct lookup, not the rest of the manager API surface.

🤖 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 `@contract/manager.go` around lines 51 - 54, The handlers map initialization
omits types.ContractTypeDelegate so manager methods like
GetSupportedContractTypes() and ScanContracts() never see delegate contracts;
add an entry to m.handlers mapping types.ContractTypeDelegate to the appropriate
handler (e.g., defaultHandler.NewHandler(cachedClient, args.Network, true) if
delegates should be treated like boarding) when building the handlers map so
GetSupportedContractTypes(), ScanContracts(), and other manager APIs include
delegates (this complements the existing GetHandler special-case).
🤖 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.

Inline comments:
In `@contract/delegate_handler.go`:
- Around line 69-83: DeriveContract currently only checks delegateKey but later
dereferences key.PubKey and cfg.SignerKey; add precondition checks at the start
of DeriveContract to validate the owner and signer keys exist (e.g., ensure key
!= nil and key.PubKey != nil, and cfg.SignerKey != nil/zero) and return
descriptive errors if they are missing, so subsequent comparisons like
delegateKey.IsEqual(key.PubKey) and delegateKey.IsEqual(cfg.SignerKey) cannot
panic.

In `@contract/manager.go`:
- Around line 250-304: NewDelegate currently always advances the wallet key and
stores a new contract even when a contract for the same delegateKey already
exists; before calling m.keyProvider.NextKeyId or deriving/storing a contract,
check the store for an existing delegate contract keyed by the provided
delegateKey (e.g. call a new or existing method like
m.store.GetDelegateContract(ctx, delegateKey) or
m.store.FindContractByDelegateKey), if found unlock and return that existing
contract (and do not call m.keyProvider.NextKeyId, m.keyProvider.GetKey,
dh.DeriveContract, m.store.AddContract, or m.emit); only if no existing contract
is found proceed with the current flow (NextKeyId -> GetKey -> dh.DeriveContract
-> AddContract -> m.emit).

In `@contract/types.go`:
- Around line 38-41: The change adds NewDelegate to the exported Manager
interface which breaks downstream implementers (tests had to update
fixContractManager and watcherMockManager); instead revert Manager to its
previous shape and expose delegate creation via a new, separate interface or
helper: define a DelegateCreator interface (or a package-level function) that
declares NewDelegate and have concrete manager implementations optionally
implement it; update tests/mocks to implement DelegateCreator (not Manager)
where needed and remove NewDelegate from Manager to preserve source
compatibility.

---

Outside diff comments:
In `@contract/manager.go`:
- Around line 51-54: The handlers map initialization omits
types.ContractTypeDelegate so manager methods like GetSupportedContractTypes()
and ScanContracts() never see delegate contracts; add an entry to m.handlers
mapping types.ContractTypeDelegate to the appropriate handler (e.g.,
defaultHandler.NewHandler(cachedClient, args.Network, true) if delegates should
be treated like boarding) when building the handlers map so
GetSupportedContractTypes(), ScanContracts(), and other manager APIs include
delegates (this complements the existing GetHandler special-case).
🪄 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: 9a7e027c-c324-4139-9621-042f21888c6f

📥 Commits

Reviewing files that changed from the base of the PR and between 1b780b0 and 9412e49.

📒 Files selected for processing (7)
  • contract/delegate_handler.go
  • contract/delegate_handler_test.go
  • contract/manager.go
  • contract/types.go
  • contract/watcher_test.go
  • spendable_vtxos_test.go
  • types/types.go

Comment on lines +69 to +83
func (h *DelegateHandler) DeriveContract(
_ context.Context,
key wallet.KeyRef,
cfg DelegateConfig,
delegateKey *btcec.PublicKey,
) (*types.Contract, error) {
if delegateKey == nil {
return nil, fmt.Errorf("delegate key must not be nil")
}
if delegateKey.IsEqual(key.PubKey) {
return nil, fmt.Errorf("delegate key must differ from owner key")
}
if delegateKey.IsEqual(cfg.SignerKey) {
return nil, fmt.Errorf("delegate key must differ from signer key")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate owner and signer keys before using them.

delegateKey is checked, but key.PubKey and cfg.SignerKey are dereferenced later without guards. Calling DeriveContract with an incomplete wallet.KeyRef or config will panic instead of returning an error.

Suggested guard
 func (h *DelegateHandler) DeriveContract(
 	_ context.Context,
 	key wallet.KeyRef,
 	cfg DelegateConfig,
 	delegateKey *btcec.PublicKey,
 ) (*types.Contract, error) {
 	if delegateKey == nil {
 		return nil, fmt.Errorf("delegate key must not be nil")
 	}
+	if key.PubKey == nil {
+		return nil, fmt.Errorf("owner key must not be nil")
+	}
+	if cfg.SignerKey == nil {
+		return nil, fmt.Errorf("signer key must not be nil")
+	}
 	if delegateKey.IsEqual(key.PubKey) {
 		return nil, fmt.Errorf("delegate key must differ from owner key")
 	}
🤖 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 `@contract/delegate_handler.go` around lines 69 - 83, DeriveContract currently
only checks delegateKey but later dereferences key.PubKey and cfg.SignerKey; add
precondition checks at the start of DeriveContract to validate the owner and
signer keys exist (e.g., ensure key != nil and key.PubKey != nil, and
cfg.SignerKey != nil/zero) and return descriptive errors if they are missing, so
subsequent comparisons like delegateKey.IsEqual(key.PubKey) and
delegateKey.IsEqual(cfg.SignerKey) cannot panic.

Comment thread contract/manager.go
Comment on lines +250 to +304
m.mu.Lock()

latestContract, err := m.store.GetLatestContract(ctx, types.ContractTypeDelegate)
if err != nil {
m.mu.Unlock()
return nil, err
}

var keyId string
if latestContract != nil {
dh := &DelegateHandler{}
keyRef, err := dh.GetKeyRef(*latestContract)
if err != nil {
m.mu.Unlock()
return nil, fmt.Errorf("failed to get key ref for latest delegate contract: %w", err)
}
keyId = keyRef.Id
}

nextKeyId, err := m.keyProvider.NextKeyId(ctx, keyId)
if err != nil {
m.mu.Unlock()
return nil, fmt.Errorf("failed to compute next key index: %w", err)
}

keyRef, err := m.keyProvider.GetKey(ctx, nextKeyId)
if err != nil {
m.mu.Unlock()
return nil, fmt.Errorf("failed to derive key for contract: %w", err)
}

dh := &DelegateHandler{}
contract, err := dh.DeriveContract(ctx, *keyRef, cfg, delegateKey)
if err != nil {
m.mu.Unlock()
return nil, err
}

keyIndex, err := m.keyProvider.GetKeyIndex(ctx, keyRef.Id)
if err != nil {
m.mu.Unlock()
return nil, fmt.Errorf("failed to get key index: %w", err)
}

if err := m.store.AddContract(ctx, *contract, keyIndex); err != nil {
m.mu.Unlock()
return nil, fmt.Errorf("failed to store delegate contract: %w", err)
}

log.Debugf("%s added new delegate contract %s", logPrefix, contract.Script)
m.mu.Unlock()

m.emit(*contract)

return contract, 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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

NewDelegate is missing the delegate-key dedup check.

This path always advances to the next wallet key and stores a new contract. A second call with the same delegateKey will therefore create duplicates and emit another creation event instead of returning the existing delegate contract.

One localized way to enforce dedup before key allocation
 	m.mu.Lock()
+	delegateKeyHex := hex.EncodeToString(delegateKey.SerializeCompressed())
+
+	existingContracts, err := m.store.GetContractsByType(ctx, types.ContractTypeDelegate)
+	if err != nil {
+		m.mu.Unlock()
+		return nil, fmt.Errorf("failed to load delegate contracts: %w", err)
+	}
+	for i := range existingContracts {
+		if existingContracts[i].Params[ParamDelegateKey] == delegateKeyHex {
+			m.mu.Unlock()
+			return &existingContracts[i], nil
+		}
+	}
 
 	latestContract, err := m.store.GetLatestContract(ctx, types.ContractTypeDelegate)
 	if err != nil {
 		m.mu.Unlock()
 		return nil, err
🤖 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 `@contract/manager.go` around lines 250 - 304, NewDelegate currently always
advances the wallet key and stores a new contract even when a contract for the
same delegateKey already exists; before calling m.keyProvider.NextKeyId or
deriving/storing a contract, check the store for an existing delegate contract
keyed by the provided delegateKey (e.g. call a new or existing method like
m.store.GetDelegateContract(ctx, delegateKey) or
m.store.FindContractByDelegateKey), if found unlock and return that existing
contract (and do not call m.keyProvider.NextKeyId, m.keyProvider.GetKey,
dh.DeriveContract, m.store.AddContract, or m.emit); only if no existing contract
is found proceed with the current flow (NextKeyId -> GetKey -> dh.DeriveContract
-> AddContract -> m.emit).

Comment thread contract/types.go Outdated
Comment on lines +38 to +41
// NewDelegate creates and stores a new delegate contract for the given delegate
// public key. The owner key is derived from the key provider. Returns an error if
// delegateKey is nil or matches the owner or signer key.
NewDelegate(ctx context.Context, delegateKey *btcec.PublicKey) (*types.Contract, error)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the contract/types.go file to understand the Manager interface context
cat -n contract/types.go | head -80

Repository: arkade-os/go-sdk

Length of output: 3947


🏁 Script executed:

# Search for the Manager interface definition and any existing methods
rg "type Manager interface" -A 20 contract/

Repository: arkade-os/go-sdk

Length of output: 1845


🏁 Script executed:

# Find all implementations of the Manager interface in the codebase
rg "Manager interface|func.*Manager.*\{" --type go -B 2 -A 10

Repository: arkade-os/go-sdk

Length of output: 17444


🏁 Script executed:

# Check if there are any existing implementations by searching for types that implement Manager
rg "implement|implements" contract/types.go --type go

Repository: arkade-os/go-sdk

Length of output: 42


🏁 Script executed:

# Look for test doubles and mock implementations that would need updating
fd -e "test.go" -o -name "*mock*" -o -name "*double*" | head -20

Repository: arkade-os/go-sdk

Length of output: 291


🏁 Script executed:

# Search for NewDelegate implementations
rg "NewDelegate" --type go -B 2 -A 10

Repository: arkade-os/go-sdk

Length of output: 4390


🏁 Script executed:

# Check if test doubles have the NewDelegate method
rg "func.*NewDelegate" --type go -B 2 -A 5

Repository: arkade-os/go-sdk

Length of output: 1337


🏁 Script executed:

# Search in specific test files for NewDelegate
rg "fixContractManager\|watcherMockManager" -A 50 --type go | rg -A 2 "NewDelegate"

Repository: arkade-os/go-sdk

Length of output: 42


🏁 Script executed:

# Look at contract/manager.go to see if it implements NewDelegate
cat -n contract/manager.go | grep -A 10 "NewDelegate"

Repository: arkade-os/go-sdk

Length of output: 529


Manager interface expansion is a breaking API change for downstream implementers.

Adding NewDelegate to the exported Manager interface will require all external implementations to add this method. This PR's own test doubles (fixContractManager, watcherMockManager) had to be updated to satisfy the widened interface. If source compatibility must be maintained across releases, consider exposing delegate creation through a separate interface or concrete helper instead of widening Manager.

🤖 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 `@contract/types.go` around lines 38 - 41, The change adds NewDelegate to the
exported Manager interface which breaks downstream implementers (tests had to
update fixContractManager and watcherMockManager); instead revert Manager to its
previous shape and expose delegate creation via a new, separate interface or
helper: define a DelegateCreator interface (or a package-level function) that
declares NewDelegate and have concrete manager implementations optionally
implement it; update tests/mocks to implement DelegateCreator (not Manager)
where needed and remove NewDelegate from Manager to preserve source
compatibility.

@bitcoin-coder-bob
Copy link
Copy Markdown
Contributor Author

done in 8c407d3

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)

contract/manager.go (1)> 51-54: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Delegate contracts are still invisible to manager-wide discovery.
Because ContractTypeDelegate is not registered in m.handlers, GetSupportedContractTypes() will omit it and ScanContracts() will never consider it. The later GetHandler special-case only fixes direct lookup, not the rest of the manager API surface.

🤖 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 `@contract/manager.go` around lines 51 - 54, The handlers map initialization
omits types.ContractTypeDelegate so manager methods like
GetSupportedContractTypes() and ScanContracts() never see delegate contracts;
add an entry to m.handlers mapping types.ContractTypeDelegate to the appropriate
handler (e.g., defaultHandler.NewHandler(cachedClient, args.Network, true) if
delegates should be treated like boarding) when building the handlers map so
GetSupportedContractTypes(), ScanContracts(), and other manager APIs include
delegates (this complements the existing GetHandler special-case).

🤖 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.

Inline comments:
In `@contract/delegate_handler.go`:
- Around line 69-83: DeriveContract currently only checks delegateKey but later
dereferences key.PubKey and cfg.SignerKey; add precondition checks at the start
of DeriveContract to validate the owner and signer keys exist (e.g., ensure key
!= nil and key.PubKey != nil, and cfg.SignerKey != nil/zero) and return
descriptive errors if they are missing, so subsequent comparisons like
delegateKey.IsEqual(key.PubKey) and delegateKey.IsEqual(cfg.SignerKey) cannot
panic.

In `@contract/manager.go`:
- Around line 250-304: NewDelegate currently always advances the wallet key and
stores a new contract even when a contract for the same delegateKey already
exists; before calling m.keyProvider.NextKeyId or deriving/storing a contract,
check the store for an existing delegate contract keyed by the provided
delegateKey (e.g. call a new or existing method like
m.store.GetDelegateContract(ctx, delegateKey) or
m.store.FindContractByDelegateKey), if found unlock and return that existing
contract (and do not call m.keyProvider.NextKeyId, m.keyProvider.GetKey,
dh.DeriveContract, m.store.AddContract, or m.emit); only if no existing contract
is found proceed with the current flow (NextKeyId -> GetKey -> dh.DeriveContract
-> AddContract -> m.emit).

In `@contract/types.go`:
- Around line 38-41: The change adds NewDelegate to the exported Manager
interface which breaks downstream implementers (tests had to update
fixContractManager and watcherMockManager); instead revert Manager to its
previous shape and expose delegate creation via a new, separate interface or
helper: define a DelegateCreator interface (or a package-level function) that
declares NewDelegate and have concrete manager implementations optionally
implement it; update tests/mocks to implement DelegateCreator (not Manager)
where needed and remove NewDelegate from Manager to preserve source
compatibility.

---

Outside diff comments:
In `@contract/manager.go`:
- Around line 51-54: The handlers map initialization omits
types.ContractTypeDelegate so manager methods like GetSupportedContractTypes()
and ScanContracts() never see delegate contracts; add an entry to m.handlers
mapping types.ContractTypeDelegate to the appropriate handler (e.g.,
defaultHandler.NewHandler(cachedClient, args.Network, true) if delegates should
be treated like boarding) when building the handlers map so
GetSupportedContractTypes(), ScanContracts(), and other manager APIs include
delegates (this complements the existing GetHandler special-case).

🪄 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

done in 8c407d3

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.

Delegate contract

1 participant