Add boarding contract type#533
Conversation
Register a `boarding` contract handler and derive the on-chain boarding address from it. Wallet setup sources boardingTapscript from the handler; contract-manager init persists a matching active boarding contract (create-if-missing), so VTXOs on the boarding script are watched.
Route default and boarding baseline registration through ensureWalletContract: a same-script collision (coinciding exit delays) accepts the existing row in either direction instead of throwing. Boarding stays a distinct row when its script differs.
WalkthroughThis PR introduces a new "boarding" contract handler to the TS SDK with wallet-level idempotent initialization. The boarding handler delegates to the existing default handler while maintaining a distinct type identifier and non-discoverable semantics. Wallet initialization is updated to derive boarding scripts from the handler and use collision-aware helpers to avoid duplicate contracts when default and boarding types produce identical scripts. ChangesBoarding Contract Handler and Wallet Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🔍 Arkana Code Review — feat/boarding-contract-type
Verdict: Code looks correct. Requesting human sign-off — this is protocol-critical (boarding = on-chain funds entry into Ark).
✅ What's right
1. Handler delegation is clean and correct.
BoardingContractHandler delegates every method to DefaultContractHandler, producing byte-identical scripts. The handler only differs in type: "boarding" and non-discoverability. This is the right level of abstraction.
2. Boarding script derivation round-trips correctly.
Old path: new DefaultVtxo.Script({ pubKey, serverPubKey, csvTimelock: boardingTimelock })
New path: BoardingContractHandler.createScript({ pubKey: hex.encode(pubKey), serverPubKey: hex.encode(serverPubKey), csvTimelock: timelockToSequence(boardingTimelock).toString() }) → DefaultContractHandler.deserializeParams → new DefaultVtxo.Script(typed).
The round-trip through timelockToSequence→toString→Number()→sequenceToTimelock is lossless for valid BIP68 values. The pubkey round-trip through hex.encode→normalizeToDescriptor→extractPubKey→hex.decode is likewise identity-preserving. Tests at boarding.test.ts:409-447 pin byte-identity.
3. ensureWalletContract collision handling is well-reasoned.
wallet.ts:255-269: Queries by script, checks type compatibility, returns early for default↔boarding collision.areSameScriptBaselineTypesCompatibleis symmetric and restrictive (only default↔boarding).- Compatible collision → existing row already active and watched (from
ContractManager.initialize()atcontractManager.ts:397-399which loads ALL persisted contracts on startup). No gap in monitoring. - Incompatible collision → falls through to
createContract→upsertContract→ throws type mismatch. Correct.
4. Test coverage is comprehensive (747 new test lines). Covers registration, script identity, address derivation, param serde, non-discoverability, spend paths, persistence, watcher subscription, idempotency, collision in both directions, incompatible-type throw, server delay change, and the shared-script edge case.
5. Backward compatible. getBoardingAddress() derives from this.boardingTapscript.onchainAddress() — unchanged and independent of the persisted contract.
⚠️ Minor observations (non-blocking)
1. ensureWalletContract skips state update on compatible collision (wallet.ts:264-267).
If an existing compatible-type row is inactive, the function returns early without promoting it to active. The watcher won't re-register it (already loaded during ContractManager.initialize(), but if inactive, it may not be watched depending on getContracts filter semantics). In practice this is a non-issue: wallet baseline contracts are always created active, and no current code path deactivates them. But worth a defensive comment.
2. Hardcoded compatibility matrix (wallet.ts:220-228).
areSameScriptBaselineTypesCompatible is a closed-form check for default↔boarding. Any future baseline type that can collide (unlikely given distinct Taproot trees) would need to be added here. The JSDoc documents this, which is sufficient.
3. Potential merge conflict with PR #518 (require-timelock).
PR #518 modifies default.ts and wallet.ts to make csvTimelock required. The boarding test "falls back to the default timelock when csvTimelock is missing" (boarding.test.ts:513-524) may need updating if #518 merges first.
4. BoardingContractHandler is a pure delegation wrapper — changes to DefaultContractHandler silently propagate. The tests pin behavior, which is the right mitigation. No action needed.
🚨 Protocol-critical flag
This PR touches boarding — the on-chain Bitcoin deposit path into Ark. While the code is correct and the refactoring preserves byte-identical behavior:
- Boarding funds are real on-chain BTC locked under the forfeit/exit Taproot tree
- The collision handling logic determines which contract row "owns" the boarding script and its VTXOs
- A bug here could cause boarding deposits to become invisible or unspendable
Requesting human review per protocol. @Kukks @altafan — please sign off.
There was a problem hiding this comment.
🔍 Arkana — Follow-up (new commit 91f2d19)
Reviewed the delta since my last pass. Only change is test/e2e/contract-params-change.test.ts:
contractsAfterFirstcount: 2 → 3 (adds boarding contract)contractsAfterSecondcount: 4 → 5 (boarding persists across param change)- Added clarifying comments explaining the count breakdown
Correct. The boarding contract is now persisted alongside default+delegate, and the e2e test expectations needed updating to reflect that. No production code changed.
My previous review stands — code is correct, still requesting human sign-off since this is protocol-critical (boarding = on-chain funds entry). @Kukks @altafan
Adds a registered
boardingcontract handler and derives the on-chain boarding address from it, replacing the ad-hocboardingTapscriptplumbing. The boarding address and thegetBoardingAddress()API are unchanged.What
BoardingContractHandler(typeboarding), reusing thedefaultDefaultVtxo.Scriptshape and spend paths; sources its CSV timelock fromArkInfo.boardingExitDelay. Not discoverable (HD index selection stays wallet-owned).@arkade-os/sdk) alongside the other built-in handlers.boardingTapscriptfrom the handler; contract-manager init persists a matchingactiveboarding contract so its Arkade address is watched.scriptis the unique contract identity. WhenboardingExitDelaycoincides with the offchain unilateral-exit delay, the boarding script is byte-identical to thedefaultbaseline;ensureWalletContractaccepts the existing row in either direction instead of creating a duplicate or throwing.default/boardingare the only compatible pair;delegatestays strict.Notes
type === "boarding"to identify boarding purpose; resolve viagetBoardingAddress()/boardingTapscript.Tests
Manually tested on mutinynet.
Summary by CodeRabbit
New Features
Tests