Repository navigation
feat(sdk-coin-stx): migrate @stacks/transactions to v7.6.0 - #9994
Conversation
|
|
bd449c4 to
ce4cf4d
Compare
ce4cf4d to
990fdcc
Compare
11e8c38 to
7aa96cc
Compare
Vijay-Jagannathan
left a comment
There was a problem hiding this comment.
Solid, well-scoped migration. The rename mapping is correct, the v7 behavioural adaptations are explained inline, and the register-for-bond → from(hex) byte-identity test covers the riskiest path. CI is green (22/24/26). Not approving yet because of one concern that touches signing bytes, plus a few smaller items.
1. Compressing appended origin pubkeys can change the multisig redeem script (needs verification)
Transaction.appendOrigin now always does appendPubkey(compressPublicKey(pubKey)).
- v2
appendPubkeykept the key as given (isCompressed(publicKey)→ tagged Compressed/Uncompressed, serialized as 33 or 65 bytes). - The spending-condition
signer(hash160 of the P2SH redeem script) is computed inmakeUnsigned*fromthis._fromPubKeysas given.getSTXAddressFromPubKeys/isValidPublicKeyaccept uncompressed keys. - If a multisig is built with uncompressed
fromPubKey(...)values, the signer is hashed over the 65-byte keys but the auth field now carries the 33-byte key. The node re-derives the redeem script from the auth fields, the hash no longer matchessigner, and the tx is rejected.
The PR claims "serialized bytes unchanged", but this is a byte change for uncompressed-key multisig. All multisig tests I found use getKeys(true).pub (compressed), so they can't catch it.
Please either:
- add a test with uncompressed pubkeys in a multisig (build → appendOrigin →
from(hex)→ compare against a v2-generated fixture), or - compress only when the key was already compressed, and instead fix the rebuild-duplication problem on the deserialize side (
initBuilderreadsfield.contents.data→ normalise there).
If WP/BitGo flows can only ever supply compressed keys, say so in the comment and in fromPubKey validation so this is an enforced invariant rather than an accident.
2. ContractBuilder.isClarityValue is a negative heuristic
type ∈ ClarityType values && !('val' in arg). A JSON descriptor missing val for a type that is also a ClarityType name ({ type: 'list' }, { type: 'tuple' }, { type: 'buffer' }) is now passed through as a "raw ClarityValue" instead of raising list requires Array val / tuple require Array val. The invalid CV then fails later (serialize) with a worse error. Prefer a positive check on the CV shape ('value' in arg, or a isClarityValue guard based on the v7 shapes) so malformed JSON still hits parseCv's validation. A test for { type: 'list' } → InvalidParameterValueError would pin it.
3. Dead code left by the cutover
_anchorMode (transactionBuilder.ts) and ANCHOR_MODE (constants.ts) are no longer read anywhere now that anchorMode is dropped from the options. Remove them (clean cutover). Also check ANCHOR_MODE isn't re-exported/consumed by another module.
4. New public API in postConditions.ts
export * from './postConditions' adds makeStxPostCondition, makeFungiblePostCondition, assetInfo, Pc, PostConditionCode, AssetInfo, … to the package surface. The doc comments call them "replacements for removed v2 helpers", but they never existed in BitGoJS' exports. Only makeStakingPostCondition is needed for SI-1936, and the in-repo builders already use Pc directly. Consider exporting only what has a caller (staking), or trim the docs so they don't read as a compat layer. Re-exporting Pc also pins a transitive library's API into ours.
5. Breaking-change handling
toJson() / explainTransaction shape change is documented well. Two follow-ups:
- This is
feat:with aBREAKING CHANGE:footer, so it will cut a major for@bitgo/sdk-coin-stxand cascade toaccount-lib/bitgo. Confirm that's intended and that WP #63608 is merged/released in lock-step (consumers pinned to the old shape will break on bump). - The "OVC — confirm if any surface reads it" item in the description is still open. Please close it before merge.
Nits
sbtcWithdrawBuilder.ts/sendmanyBuilder.ts:ContractCallPayloadis appended after the value imports; move it alongside the other type imports.utils.ts:transactionVersionToNetworkNamewas inserted betweengetAddressFromPublicKeyHashand its JSDoc block, which now documents the wrong function. Move the helper above the JSDoc.keyPair.getSTXAddress: any non-TestnetTransactionVersionsilently maps to mainnet; fine for the two enum values, but worth a comment.
Positive: the lockfile dedupes to a single @stacks/* v7 line (utxo-descriptors already used it) and drops @stacks/common v1/v2/v4, c32check and cross-sha256, which is a good side effect.
67e234a to
22b119a
Compare
Bump @stacks/transactions 2.0.1 -> 7.6.0 (and @stacks/network ^4.3.0 -> ^7.6.0). v7 is required to build the SIP-044 staking post-condition (PostConditionType.Staking = 3), the blocker for PoX-5 register-for-bond (SI-1936). Port the sdk-coin-stx src + tests to the v7 API, keeping serialized transactions byte-identical. v7 renames (BufferReader -> BytesReader, StacksTransaction -> StacksTransactionWire, StacksMessageType -> StacksWireType, sigHashPreSign, publicKeyFromSignatureVrs) and moves most post-condition builders to the `Pc` builder namespace. Behavioural adaptations required by v7 (not just renames): - contractBuilder: detect raw ClarityValues with a positive shape check (the v2 numeric-enum reverse lookup breaks under v7 string enums); a malformed JSON descriptor still hits `parseCv`'s validation. - abstractContractBuilder.initBuilder: read clarity args from the raw payload instead of toJson()'s stringified shape. - Default an unset post-condition mode to Deny (v7 throws on explicit undefined). - sign()/appendOrigin(): avoid the stricter v7 TransactionSigner constructor. - Reset the multisig auth-field list that v7 pre-populates in makeUnsigned*, and append the origin pubkey as given (v2 semantics) with the deserialized pubkey normalised to compressed in initBuilder — keeps the auth-field bytes identical to v2 and avoids duplicate entries on a rebuild. - isValidMemo: reject non-string memos explicitly (v7's exceedsMaxLengthBytes no longer throws on non-strings). - Remove dead `_anchorMode` / `ANCHOR_MODE` (anchorMode was dropped from options). Add `src/lib/postConditions.ts` with `makeStakingPostCondition` (the SIP-044 type-3 staking post-condition builder, the SI-1936 enabler). No other new exports are added — the module's export surface is otherwise unchanged. Also fix `findContractTokenNameUsingContract`: its guard was `sip10Token ? sip10Token[0].assetId.split('::')[1] : undefined`, but `filter()` always returns an array — and an empty array is truthy — so an unknown contract dereferenced `sip10Token[0].assetId` and threw a TypeError instead of returning `undefined`. Callers now get the intended 'invalid contract address or contract name, not supported' error. The same always-truthy pattern is tidied in `findTokenNameByContract` (harmless there, but identical intent). The fungible-token builder also fails fast on an unresolved token name: it now throws an InvalidParameterValueError instead of serialising an 'undefined' asset name, which could never match a Deny-mode post-condition and would only abort on-chain, burning the fee. Tests: `stx.pox5RegisterForBond`-style module coverage plus a `pox5Builder` test that builds register-for-bond with `PostConditionMode.Deny` + a type-3 staking post-condition, serializes, rebuilds via `factory.from(hex)` and asserts byte-identical output. BREAKING CHANGE: @stacks/transactions v7 changes the Clarity representation, so the JSON returned by sdk-coin-stx `toJson()` / `explainTransaction` (`payload.functionArgs`) changes shape: Clarity type tags are now strings (`'uint'` vs `1`) and principal/buffer values use the v7 form (`{ type: 'address', value: <c32 address> }` — prefix varies by network/version, e.g. SP/ST/SN/SM; `{ type: 'buffer', value: <hex> }`) instead of v2 (`{ type: 5, address: { version, hash160 } }`, `{ type: 2, buffer }`). Consumers that read `functionArgs` (Wallet Platform, policy service, OVC) must expect the v7 shape. Serialized transaction bytes are unchanged (byte-parity preserved) — signing and the HSM are unaffected. Ticket: SI-2024
22b119a to
83d6e3c
Compare
Vijay-Jagannathan
left a comment
There was a problem hiding this comment.
Re-reviewed 83d6e3c32a against my earlier comments. Every code finding is addressed. Approving.
Previous findings
- Uncompressed multisig keys: fixed.
appendOriginappends the key as given again. I checked both libs: v2serializeTransactionAuthFieldand v7serializeTransactionAuthFieldByteswrite the uncompressed tag plus the 33-byte compressed key. So the auth-field bytes match v2. Compression now happens only ininitBuilderon the rebuild path, where v2 also read back a 33-byte key. The explanation in the code comment is accurate. isClarityValue: fixed. It is now a positive shape check (valuepresent, or one of the valueless none/true/false). The new test pins that{ type: 'list' }and{ type: 'tuple' }throwInvalidParameterValueError.- Dead code: fixed.
_anchorModeandANCHOR_MODEare removed, with no remaining references inmodules/. - Public API: fixed.
postConditions.tsis trimmed tomakeStakingPostConditionplus its comparator and amount types.Pc,assetInfoand the STX/FT helpers are no longer exported. - Nits: fixed. The imports are reordered, the
utils.tshelper no longer sits between a function and its JSDoc, andgetSTXAddresshas an explanatory comment.
Fixes beyond my review (good catches)
findTokenNameByContractandfindContractTokenNameUsingContracttestedarr ? arr[0] : undefined. An empty array is truthy, so the second one threw on[0].assetId. Both now check.length.FungibleTokenTransferBuildernow throwsInvalidParameterValueErrorfor an unknown token contract instead of serialising anundefinedasset name. That post-condition could never match, so a Deny-mode tx would abort on-chain and still burn the fee. This is a small behaviour change, but the right one.
Non-blocking, please consider
- There is still no test that builds a multisig from uncompressed pubkeys through build →
appendOrigin→from(hex)and compares the bytes to a v2-generated fixture. The code now looks right to me on a read of both libs, but a fixture would pin it. - The BREAKING CHANGE footer will trigger a major bump for
sdk-coin-stx, and its consumers (account-lib,bitgo) need to be bumped with it. Before merging, confirm the WP #63608 release ordering and close the "OVC readsfunctionArgs?" item in the description. - CI: Node 26, browser-test, docker-build and the lint, format and dependencies jobs are green. The
unit-testjobs for Node 22 and 24 and "Internal Checks" were still pending when I reviewed. Please wait for them before merging.
83d6e3c to
dc7c1b8
Compare
Vijay-Jagannathan
left a comment
There was a problem hiding this comment.
Re-approving after the force-push dismissed my earlier approval.
Delta since the head I approved (83d6e3c32a → dc7c1b869c): the source commit 754db4e2f1 is byte-for-byte unchanged. The test commit only adds:
- sBTC transfer tests for
tstx:tsbtcandstx:sbtc: token name resolved from statics,sbtc-token::sbtc-tokenpost-condition, byte-identical rebuild viafactory.from(hex). - Two
findContractTokenNameUsingContractassertions for the sBTC contracts.
These exercise a real path (statics-resolved asset name on the new Pc.ft post-condition) and the rebuild check is the right invariant. No new concerns.
My earlier non-blocking notes still stand (uncompressed-key multisig fixture vs v2; major-bump/WP #63608 ordering; close the OVC item). CI was fully re-queued on the new head, so please wait for unit-test (22/24/26) and Internal Checks to go green before merging.
dc7c1b8 to
52762ac
Compare
Add unit tests for the branches left uncovered across the module: coin registration and base-coin methods, transaction builder validators, transfer/sendmany/fungible-token/contract builders, the sBTC withdraw deserialization guards, PoX-5 calculate/claim rewards, the transaction builder factory, transaction error branches, key pair, btc address decoding, sip10 token metadata and the stx node-request/recovery paths. Also pins the token-name lookup fixed in the migration commit: a contract that is not a known SIP-10 token resolves to undefined instead of throwing a TypeError, building a token transfer for it fails with the intended 'invalid contract address or contract name, not supported', and the fungible-token builder now fails the build rather than serialising an 'undefined' asset name. Closes the sBTC token-transfer gap (tstx:tsbtc / stx:sbtc resolve from statics and emit a sbtc-token::sbtc-token post-condition, byte-identical across a factory.from(hex) rebuild). Pins the multisig key encodings against v2.0.1-generated fixtures: the uncompressed build + appendOrigin bytes match v2 exactly, a compressed-key multisig tx verifies and round-trips byte-identically, and the uncompressed-key rebuild/verification limitation is asserted (pre-existing and identical in v2.0.1, v6.17.0 and v7.6.0) instead of assumed. Test/coverage status vs the pre-upgrade baseline (module): - tests: ~226 -> 328 passing; test files 16 -> 21 - coverage (c8, src/**): ~93.5% stmts / ~81.3% branch -> 98.98% stmts / 91.27% branch / 99.07% funcs Ticket: SI-2024
52762ac to
8eb89e6
Compare
v2 → v7 verification summary (old vs new), and one pre-existing anomalyRather than trust the fixtures alone, I installed the real old SDKs in scratch dirs ( Build output — identical
Behaviour parityMultisig auth-field pre-population (cleared to match v2), Intended differences (behaviour changes in this PR)
One anomaly — pre-existing, not introduced hereUncompressed-key multisig produces a tx that does not verify. The signer hash is derived from the uncompressed keys, while the auth fields only carry the compressed key bytes, so a re-derivation from the tx's own fields can never match the signer. Identical across versions:
Compressed keys (what BitGo wallets use — xpub-derived, and Suggested follow-up (separate ticket, deliberately out of scope here): fail fast on uncompressed keys for multisig builds so this path errors instead of emitting an unverifiable tx. The alternative — normalising to compressed — would change those multisig addresses, so it needs a product decision first. Tests / coverage (module)
The two new multisig tests (compressed verification + the uncompressed limitation) landed in the test commit; CI is re-running on the new head. |
Vijay-Jagannathan
left a comment
There was a problem hiding this comment.
Re-approving (8eb89e683b); the force-push dismissed the previous approval. The source commit 754db4e2f1 is unchanged, and the test commit touches no src/ files.
The new tests answer my last open note, the uncompressed-key multisig check against v2:
- v2 fixtures.
UNCOMPRESSED_MULTISIG_UNSIGNED_TRANSFERand..._SIGNED_TRANSFERwere generated with@stacks/transactions2.0.1. The uncompressed build +appendOriginpath matches them byte-for-byte, so the "append as given" fix is now pinned rather than argued from reading the code. - Known limitation. Uncompressed-key multisig txs don't self-verify (signer hash over 65-byte keys, auth fields carry 33-byte keys) and a
from(hex)rebuild isn't byte-identical. The tests assert this is pre-existing and v2-identical. That corrects my first review: this is not a regression from the migration. - Compressed-key multisig. It is covered by a verify-and-rebuild-byte-identical test.
Nit: the two "limitation" tests pin behaviour that is arguably a latent bug, so they will need updating if it is ever fixed. That's fine as documented, but consider a follow-up ticket for uncompressed-key multisig handling.
Remaining non-blocking items from earlier: confirm the major-bump and WP #63608 release ordering, and close the OVC item in the description. CI was re-queued on this head, so wait for unit-test (22/24/26) and Internal Checks before merging.
diksha190
left a comment
There was a problem hiding this comment.
Approved. SafeChain passed, Socket 92 (deep = shallow, no dependencies, no high/critical alerts).
Package: [email protected].
Context
@bitgo/sdk-coin-stxpinned@stacks/transactions2.0.1. The v7 line is required to build theSIP-044 staking post-condition (
PostConditionType.Staking = 3) — the blocker for PoX-5register-for-bond(SI-1936).Change
modules/sdk-coin-stx/package.json,yarn.lock:@stacks/transactions2.0.1 → 7.6.0,@stacks/network^4.3.0 → ^7.6.0.sdk-coin-stxsrc + tests to the v7 API (renames:BufferReader → BytesReader,StacksTransaction → StacksTransactionWire,StacksMessageType → StacksWireType,makeSigHashPreSign → sigHashPreSign,publicKeyFromSignature → publicKeyFromSignatureVrs,makeStandard*PostCondition → Pc.*).src/lib/postConditions.ts:makeStakingPostCondition— the SIP-044 type-3 stakingpost-condition builder (the SI-1936 enabler). No other new exports; the module's export surface is
otherwise unchanged.
findContractTokenNameUsingContract(src/lib/utils.ts):filter()always returns a (truthy)array, so an unknown contract dereferenced
sip10Token[0].assetIdand threw aTypeErrorinsteadof returning
undefined. Callers now get the intendedinvalid contract address or contract name, not supported. Same always-truthy pattern tidied infindTokenNameByContract.fungibleTokenTransferBuilder): the builder now throwsInvalidParameterValueError: unknown fungible token contract: <addr>.<name>instead of serialisinga post-condition with
assetName: "undefined"— which can never match and would only abort on-chainunder
Denymode, burning the fee. (Verified by probe before/after.)8 behavioural adaptations v7 requires (not just renames)
contractBuilder: detect rawClarityValues with a positive shape check (the v2 numeric-enumreverse lookup breaks under v7 string enums); a malformed JSON descriptor (
{ type: 'list' }) stillhits
parseCv's validation.abstractContractBuilder.initBuilder: read clarity args from the raw payload, nottoJson()'sstringified shape.
Deny(v7 throws on an explicitundefined).sign()/appendOrigin(): bypass the stricter v7TransactionSignerconstructor.makeUnsigned*, for byte parity.appendOriginappends the origin pubkey as given (v2 semantics);initBuildernormalises thedeserialized pubkey to compressed — keeps the auth-field bytes identical to v2 and avoids duplicate
entries on a rebuild.
isValidMemo(): reject non-string memos explicitly (v7'sexceedsMaxLengthBytesno longer throwson non-strings).
_anchorMode/ANCHOR_MODE(anchorMode was dropped from the options).toJson()/explainTransactionshape@stacks/transactionsv7 changed the Clarity representation, so the JSON returned by sdk-coin-stxtoJson()/explainTransaction(payload.functionArgs) changes shape:{ type: 1, value }(numeric tag){ type: 'uint', value }(string tag){ type: 5, address: { version, hash160 } }{ type: 'address', value: <c32 address> }(prefix varies by network/version: SP/ST/SN/SM){ type: 2, buffer }{ type: 'buffer', value: <hex> }Tuple key
dataand list keylistare preserved. Serialized transaction bytes are unchanged(byte-parity preserved) — signing and the HSM are unaffected; this is a JSON/API-shape change only.
One pre-existing exception, now pinned by a test: rebuilding an uncompressed-key multisig tx
re-derives the sender from the compressed keys recovered on deserialization (v2 did the same).
Consumer expectations (recorded for the breaking change):
read
functionArgs; only its STX test imports v2@stackssymbols (tracked in SI-2026).hsm-firmware/src/bg/stx.ccalready hasPOST_CONDITION_TYPE_STAKING = 3) — verify-only.Commits
754db4e2f1feat(sdk-coin-stx): migrate @stacks/transactions to v7.6.0makeStakingPostCondition+findContractTokenNameUsingContract/ fail-fast fixes (review follow-ups folded in)8eb89e683btest(sdk-coin-stx): cover builders, recovery and validatorsTests
stx.pox5RegisterForBond-style module coverage extended; new:pox5Builderbuildsregister-for-bondwithPostConditionMode.Deny+ a type-3 staking post-condition(
makeStakingPostCondition), serializes, rebuilds viafactory.from(hex), and assertsbyte-identical output with the post-condition restored (the
initBuilderrestore path).makeStakingPostConditionunit-tested across all comparators —eq/gt/gte/lt/lteas bothwire strings and
FungibleConditionCode, all three amount forms (string/number/bigint), and theunsupported-code rejection.
undefined, building a token transfer for it fails with the intendedinvalid contract address or contract name, not supported(the guard the fix made reachable), andthe fungible-token builder fails the build (
unknown fungible token contract) rather than emittingan
undefinedasset name.tstx:tsip6dp):tstx:tsbtc/stx:sbtctransfers resolve the token name from statics and emit a<contract>.sbtc-token::sbtc-tokenpost-condition, byte-identical across afactory.from(hex)rebuild. Note this is the SIP-10 transfer path; the separate sBTC withdrawal
(
initiate-withdrawal-request) and sBTC register-for-bond lockup flows were already covered.the uncompressed build +
appendOriginbytes match v2 exactly; a compressed-key multisig txverifyOrigin()s and round-trips byte-identically; and the uncompressed-key rebuild/verificationlimitation is asserted (pre-existing and identical in v2.0.1, v6.17.0 and v7.6.0) rather than assumed.
appendOrigin;unsigned and 2-of-3 signed) — closes the reviewer's non-blocking ask. The
from(hex)rebuild of sucha tx re-derives the sender from the compressed keys recovered on deserialization; that is a
pre-existing limitation (v2 behaves identically — its deserialized signature fields recover the
compressed key and the appended origin field deserializes to a 33-byte key), asserted explicitly
rather than left implicit.
uplift (registration, coin metadata, builder validators, transfer/sendmany/fungible-token/contract
builders, sBTC-withdraw deserialization guards, PoX-5 rewards, the tx factory, tx error branches,
key pair, BTC address decoding, and the stx node-request/recovery paths).
Verification (local)
tsc --build .(src + test): 0 errors.eslint --quiet .: 0 errors;prettier --check: clean.mocha: 328 passing / 0 failing.src/**): 98.98% stmts / 91.27% branch / 99.07% funcs (migration baseline was93.5% / 81.3%);
lib/postConditions.tsis 100% stmts / 100% funcs / 100% lines. The remaininggaps are closing-brace artifacts and unreachable defensive guards (e.g. the
tstx/tstxsignetconstructor checks behind the
Stxsuper call, the duplicate total-amount re-check, andsignTransaction's!transactionguard). Note: the module's stockyarn coveragenyctransformdoes not instrument TS in this environment, so the numbers above are measured with
c8.@bitgo/account-lib,bitgo,@bitgo/web-demo— success.@bitgo-beta/sdk-coin-stxand run inside WP → WPtsc -b0 errors + all STX/SIP-10 suites green (see SI-2025).Review follow-ups (folded into
754db4e2f1)appendOrigincompressing pubkeys can change the multisig redeem script for uncompressed keysinitBuilder; existing uncompressed-multisig round-trip test covers itisClarityValuenegative heuristic lets malformed JSON throughvalue, or a valueless CV); test pins{ type: 'list' }/{ type: 'tuple' }→InvalidParameterValueError_anchorMode/ANCHOR_MODEpostConditions.tscompat-style surface +Pcre-exportmakeStakingPostCondition;Pcre-export droppeddeprecated.tsre-uses v2 names returning v7 typestoJson()/explain shape change undocumentedBREAKING CHANGE(+ consumer expectations above)functionArgs(only its STX test imports v2@stackssymbols → SI-2026)ContractCallPayload),transactionVersionToNetworkNamemoved above the JSDoc, non-Testnet→mainnet comment addedRelated
(OVC), SI-2028 (HSM verify-only).