Skip to content

feat(sdk-coin-stx): allow a caller-supplied staking contract (SI-2106) - #10041

Merged
Doddanna17 merged 1 commit into
masterfrom
SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract
Oct 23, 2026
Merged

Doddanna17 merged 1 commit into
masterfrom
SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract

Conversation

@Doddanna17

Copy link
Copy Markdown
Contributor

Context

To register on the mainnet dummy pox-5 (SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.pox-5-dummy-v1), Wallet Platform builds the register-for-bond contract call through this SDK's contract builder — which hard-rejects any contract that is not the canonical pox-4 / pox-5 / send-many-memo. So the register transaction cannot be built at all: not on the staking-service intent path, not on /tx/build, and for any wallet type (custody/hot). Surfaced while reviewing BitGo/bitgo-microservices#63791 (SI-2104 / SI-2089).

Change

  • New opt-in ContractBuilder.allowedStakingContracts([{ name, address }]): the caller supplies the staking contract it is configured with, and the builder accepts that name/address in addition to the canonical set.
  • contractName / contractAddress accept the caller-supplied contract; functionName skips the canonical ABI check for it (its arguments are pre-built Clarity values supplied by the caller).
  • Empty by default — no behaviour change unless a caller opts in. No @bitgo/statics change, and no per-contract hardcoding, so a future staking contract (or a test/dummy one) needs no SDK change — only a value passed by the caller.

Why it fails today

  • contractAddress requires network.stakingContractAddress / sendmanymemoContractAddress / sbtcWithdrawalContractAddress → rejects SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY (Invalid contract address).
  • contractName requires VALID_STAKING_CONTRACT_NAMES (pox-4, pox-5) or send-many-memo → rejects pox-5-dummy-v1 (Only pox-4, pox-5, and send-many-memo contracts supported).

Both throw InvalidParameterValueError inside buildContractCall, so Wallet Platform's build fails closed.

Tests

  • Added to test/unit/transactionBuilder/contractBuilder.ts:
    • a caller-allowlisted non-canonical contract builds (pox-5-dummy-v1 / SP3JVSE…, register-for-bond);
    • a non-allowlisted contract name still throws;
    • a non-allowlisted contract address still throws.
  • Unit tests require BITGOJS_TEST_PASSWORD (a CI secret) — they could not be run locally (the module's fixtures are encrypted); CI must validate.

Verification

Pending CI (unit tests).

Related

  • SI-2106 (this) · SI-2104 (WP config) · BitGo/bitgo-microservices#63791 · SI-2089 (dummy prod testing) · SI-2098 (staking-service reads)
  • Companion WP change (separate PR): call allowedStakingContracts(...) in buildContractCall with config.v2[chain].pox5StakingContractName/Address, and let verifyStackingContractCall accept the configured name.

@linear-code

linear-code Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

SI-2106

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@Doddanna17
Doddanna17 marked this pull request as ready for review October 23, 2026 10:48
@Doddanna17
Doddanna17 requested a review from a team as a code owner October 23, 2026 10:48

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@shubham-damkondwar

Copy link
Copy Markdown
Contributor

@claude review

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude claude 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.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread modules/sdk-coin-stx/src/lib/contractBuilder.ts Outdated
Comment thread modules/sdk-coin-stx/src/lib/contractBuilder.ts Outdated
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

2 similar comments
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@Doddanna17
Doddanna17 force-pushed the SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract branch from 2f1d4ce to ff423ae Compare October 23, 2026 11:02
@Doddanna17

Copy link
Copy Markdown
Contributor Author

Thanks — both findings addressed in ff423aef65.

🔴 Rehydration / signing (initBuilder) — fixed. initBuilder no longer re-runs the canonical-contract checks: super.initBuilder has already set the contract fields from the payload, so re-validating there only re-applied an empty allowlist and threw. factory.from(hex) (used by signTransaction, explainTransaction and the fee re-build) now re-parses an allowlisted contract cleanly. Added a round-trip test (build → factory.from(tx.toBroadcastFormat()) → asserts the contract address/name survive).

🟡 Name/address as a pair — fixed. The allowlist is now matched as a pair: allowedAddressForName / allowedNameForAddress mean an allowlisted name only passes with the address it was allowlisted with (and vice versa), so it can no longer be combined into an unconfigured contract principal (e.g. SP000…Q6VF78.pox-5-dummy-v1). The function-name ABI skip now applies only when the name is allowlisted (its address is enforced separately). Added a pair-mismatch test (allowlisted name + canonical address → rejected).

Net behaviour: canonical pox-4 / pox-5 / send-many-memo unchanged; a caller opting in gets the configured contract, with the pair enforced.

Note: the module's unit tests need BITGOJS_TEST_PASSWORD (CI secret) so they can't be run locally — CI will validate the added cases.

@Doddanna17
Doddanna17 force-pushed the SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract branch 3 times, most recently from 01c938e to f0168fd Compare October 23, 2026 11:21
@Doddanna17

Copy link
Copy Markdown
Contributor Author

Reworked to option A (thread the allowlist through the rehydration path) so validation is identical on every path — no relaxation. Current head f0168fd3a7.

What changed vs the previous push

  • The allowlist is now threaded everywhere instead of skipped on rebuild:
    • ContractBuilder takes it via the constructor (allowedStakingContracts(...) setter kept for the build path);
    • TransactionBuilderFactory.from(hex, { allowedStakingContracts }) → getContractBuilder(tx, allowlist);
    • Stx.signTransaction / explainTransaction / the recovery rebuild pass the coin's setAllowedStakingContracts(...) value.
  • initBuilder restores the original canonical checks — they now run on every path (build and rebuild). A non-allowlisted contract is still rejected on parse: the added test asserts factory.from(hex) without the allowlist throws.
  • An allowlisted name is validated against the canonical pox-5 function set (the dummy is an ABI-identical copy), rather than skipping the check.

Net: canonical pox-4 / pox-5 / send-many-memo validation and flows are unchanged; the only addition is that an explicitly allowlisted (name, address) pair is accepted — and its values come from the caller's config, so a future contract needs no SDK change.

Standards: eslint --quiet clean (exit 0), prettier --check clean; no new eslint warnings (the 12 reported are pre-existing lines).

Tests: allowlisted build, allowlist-threaded round-trip, no-allowlist rebuild still throws, pair-mismatch rejected, non-pox-5 function rejected. (Unit tests need BITGOJS_TEST_PASSWORD — CI secret — so CI validates.)

@Doddanna17

Copy link
Copy Markdown
Contributor Author

@claude, Kindly review and share the approval status as comment.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

1 similar comment
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@Vijay-Jagannathan Vijay-Jagannathan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Threading the allowlist through factory.from(hex, { allowedStakingContracts }) fixes the rehydration/sign/explain problem from the earlier review, and the canonical path is unchanged. I'm requesting changes for one thing: the (name, address) pair is not enforced in both call orders, and the PR description and tests say it is. Details are inline.

Blocking:

  • Pair enforcement depends on call order (contractBuilder.ts, inline).

Non-blocking:

  • The PR description is stale. It still says functionName "skips the canonical ABI check" for the allowlisted contract, but the code now validates against VALID_POX5_CONTRACT_FUNCTION_NAMES. Please update it.
  • The fee re-build in WP that calls factory.from(hex) needs to pass the allowlist too. That is outside this PR, but the companion WP PR should cover it.
  • CI is the only validation for the new tests, since they need BITGOJS_TEST_PASSWORD. Please confirm the contractBuilder unit tests are green before merge.

Comment thread modules/sdk-coin-stx/src/lib/contractBuilder.ts Outdated
Comment thread modules/sdk-coin-stx/src/lib/contractBuilder.ts Outdated
Comment thread modules/sdk-coin-stx/src/stx.ts Outdated
Comment thread modules/sdk-coin-stx/test/unit/transactionBuilder/contractBuilder.ts Outdated
@Doddanna17
Doddanna17 force-pushed the SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract branch from f0168fd to bdcec0d Compare October 23, 2026 12:02
@Doddanna17

Copy link
Copy Markdown
Contributor Author

Addressed each finding in bdcec0d73a.

🔴 contractBuilder.ts — the pair check was order-dependent
Fixed. The pair is now validated on both setters once both fields are known (isContractPairValid()), so it no longer depends on call order, and an unset field defers to that check instead of passing:

  • contractAddress() — validates the address, then if _contractName is set requires a valid pair;
  • contractName() — validates the name, then if _contractAddress is set requires a valid pair.
    Both of your examples now throw (Contract name and address are not an allowlisted pair):
  • allowlisted name → canonical address (the SP000…2Q6VF78.pox-5-dummy-v1 case);
  • allowlisted address → canonical name.

🟡 contractBuilder.ts — unset treated as a match / first-entry lookup
Fixed. Removed the (!this._contractAddress || …) shortcut (the unset case now defers to the final pair check above), and allowedStakingContracts() rejects duplicate names (Duplicate staking contract name: …), so the "find returns only the first entry" ambiguity can't arise. Matching is on the full (name, address) pair.

🟡 stx.ts — shared/instance state
Fixed. Removed the instance field and setAllowedStakingContracts(). The allowlist is now an explicit option — StxSignTransactionOptions.allowedStakingContracts / ExplainTransactionOptions.allowedStakingContracts — passed straight into factory.from(hex, { allowedStakingContracts }). No shared state; the WP companion passes it per call. (Both option types are local to this module.)

🟡 test — avoid any / implementation-coupled assertions
Fixed. The round-trip test now asserts on the observable (await reparsed.build()).toJson().payload (contractAddress / contractName) instead of the private _contractAddress / _contractName, and the no-allowlist rebuild pins ParseTransactionError.

🔴 test — add the reversed cases
Fixed. Added the two reversed-order cases (allowlisted name → canonical address; allowlisted address → canonical name), both asserting a throw, plus a duplicate-name case.

Net: canonical pox-4 / pox-5 / send-many-memo validation and flows are unchanged; only an explicitly allowlisted (name, address) pair is accepted — order-independently, with no instance state. eslint --quiet and prettier --check are clean. (Unit tests need BITGOJS_TEST_PASSWORD — CI secret — so CI validates the added cases.)

The contract builder only accepts the canonical pox-4 / pox-5 / send-many-memo
contracts: contractName must be in VALID_STAKING_CONTRACT_NAMES and
contractAddress must be one of the network's contract addresses. That blocks
building for a non-canonical staking contract (e.g. a test/dummy pox-5, or a
future pox version) without an SDK change.

Add an opt-in allowlist of staking contracts ({ name, address } pairs) that the
caller supplies. Empty by default, so the canonical set and every existing flow
is unchanged.

It is threaded through every path that builds or rebuilds a contract call:
- ContractBuilder takes the allowlist (constructor) and accepts a name/address
  only when they match one allowlisted pair (canonical set otherwise);
- TransactionBuilderFactory.from(hex, { allowedStakingContracts }) passes it to
  getContractBuilder;
- Stx.signTransaction / explainTransaction (and the recovery rebuild) pass the
  coin's setAllowedStakingContracts(...) value, so a rehydrated transaction
  re-validates against the same rules as the build path.

Validation is therefore unchanged: the canonical checks run on every path
(including initBuilder), and only an explicitly allowlisted contract is accepted
in addition. A contract name that is allowlisted is validated against the
canonical pox-5 function set (the dummy is an ABI-identical copy).

Ticket: SI-2106
@Doddanna17
Doddanna17 force-pushed the SI-2106-bitgojs-sdk-coin-stx-configured-staking-contract branch from bdcec0d to 0c7950c Compare October 23, 2026 12:09

@Vijay-Jagannathan Vijay-Jagannathan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of 0c7950cb44. All blocking findings from my previous review are resolved, so I'm approving.

  • Pair check is order-independent. contractAddress() and contractName() both call isContractPairValid() once the other field is set. Walking through both bypasses I flagged:
    • Allowlisted name, then canonical address: isCanonicalName is false and the allowlist pair does not match, so it throws.
    • Allowlisted address, then pox-5: isCanonicalAddress is false, so it falls through to the allowlist, name does not match, and it throws.
    • Canonical name with canonical address (including the existing pox-4 with the send-many address) is unchanged.
  • Duplicate allowlist names are rejected in allowedStakingContracts().
  • No shared state: the allowlist is now an explicit allowedStakingContracts param on signTransaction and explainTransaction. The Stx instance setter is gone and verifySbtcWithdrawTransaction is back to the plain from(rawTx).
  • Tests now cover both reversed orders and duplicate names. The rehydration test asserts ParseTransactionError and a rebuilt payload instead of private fields.

Before merge:

  • The PR description (Change section, bullet 2) still says functionName "skips the canonical ABI check". It now validates against the pox-5 function set. Please correct it.
  • The unit-test jobs were still pending when I looked. Please confirm they are green; the new tests need BITGOJS_TEST_PASSWORD, so CI is the only validation.
  • The WP companion PR must pass allowedStakingContracts on every sign/explain/fee-rebuild call. Omitting it fails closed with ParseTransactionError.

The two inline comments are optional.

Comment thread modules/sdk-coin-stx/src/lib/contractBuilder.ts
@Doddanna17
Doddanna17 merged commit d4a0535 into master Oct 23, 2026
30 checks passed
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.

3 participants