Skip to content

Commit 91eeaac

Browse files
authored
Merge pull request #9994 from BitGo/SI-2024-bitgojs-stacks-transactions-v7
feat(sdk-coin-stx): migrate @stacks/transactions to v7.6.0
2 parents 8fe7ada + 8eb89e6 commit 91eeaac

39 files changed

Lines changed: 1595 additions & 349 deletions

‎modules/sdk-coin-stx/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
"@bitgo/secp256k1": "^1.11.1",
4545
"@bitgo/statics": "^59.23.1",
4646
"@noble/curves": "1.8.1",
47-
"@stacks/network": "^4.3.0",
48-
"@stacks/transactions": "2.0.1",
47+
"@stacks/network": "^7.6.0",
48+
"@stacks/transactions": "7.6.0",
4949
"bech32": "^2.0.0",
5050
"bignumber.js": "^9.0.0",
5151
"bn.js": "^5.2.1",

‎modules/sdk-coin-stx/src/lib/abstractContractBuilder.ts‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { BaseCoin as CoinConfig } from '@bitgo/statics';
22
import BigNum from 'bn.js';
33
import {
4+
addressToString,
45
ClarityValue,
56
ContractCallOptions,
7+
ContractCallPayload,
68
makeUnsignedContractCall,
79
PayloadType,
810
PostCondition,
911
PostConditionMode,
12+
PostConditionWire,
1013
UnsignedContractCallOptions,
1114
UnsignedMultiSigContractCallOptions,
1215
} from '@stacks/transactions';
@@ -25,7 +28,7 @@ export abstract class AbstractContractBuilder extends TransactionBuilder {
2528
protected _contractName: string;
2629
protected _functionName: string;
2730
protected _postConditionMode?: PostConditionMode;
28-
protected _postConditions?: PostCondition[];
31+
protected _postConditions?: (PostCondition | PostConditionWire | string)[];
2932
protected _functionArgs: ClarityValue[];
3033

3134
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -38,16 +41,20 @@ export abstract class AbstractContractBuilder extends TransactionBuilder {
3841
throw new InvalidTransactionError('payload must not be undefined');
3942
}
4043
if (txData.payload.payloadType === PayloadType.ContractCall) {
41-
this._contractAddress = txData.payload.contractAddress;
42-
this._contractName = txData.payload.contractName;
43-
this._functionName = txData.payload.functionName;
44-
this._functionArgs = txData.payload.functionArgs;
44+
// Read the contract call fields from the deserialized payload. `toJson()` stringifies the
45+
// clarity args (plain `{ type, data | list }` objects) for API consumers, and those are not
46+
// valid ClarityValues under v7 — use the raw payload values so a rebuild stays byte-identical.
47+
const payload = tx.stxTransaction.payload as ContractCallPayload;
48+
this._contractAddress = addressToString(payload.contractAddress);
49+
this._contractName = payload.contractName.content;
50+
this._functionName = payload.functionName.content;
51+
this._functionArgs = payload.functionArgs;
4552
// Restore post-conditions from the deserialized tx — `toJson()` does not carry them, so
4653
// without this a re-build (e.g. the fee re-build via `factory.from(hex)`) would drop them and
4754
// a `Deny`-mode contract call would abort on-chain (SIP-005 post-condition check).
4855
if (tx.stxTransaction) {
4956
this._postConditionMode = tx.stxTransaction.postConditionMode;
50-
this._postConditions = tx.stxTransaction.postConditions.values as PostCondition[];
57+
this._postConditions = tx.stxTransaction.postConditions.values;
5158
}
5259
super.initBuilder(tx);
5360
} else {
@@ -93,9 +100,9 @@ export abstract class AbstractContractBuilder extends TransactionBuilder {
93100
contractName: this._contractName,
94101
functionName: this._functionName,
95102
functionArgs: this._functionArgs,
96-
postConditionMode: this._postConditionMode,
103+
// v7 requires an explicit post-condition mode; v2 defaulted an unset mode to `Deny`.
104+
postConditionMode: this._postConditionMode ?? PostConditionMode.Deny,
97105
postConditions: this._postConditions,
98-
anchorMode: this._anchorMode,
99106
network: this._network,
100107
fee: new BigNum(this._fee.fee).toString(),
101108
nonce: new BigNum(this._nonce).toString(),

‎modules/sdk-coin-stx/src/lib/constants.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,4 @@ export const VALID_CONTRACT_FUNCTION_NAMES = [
4242

4343
export const DEFAULT_SEED_SIZE_BYTES = 64;
4444

45-
// https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md#transaction-encoding
46-
export const ANCHOR_MODE = 3;
4745
export const DEFAULT_MULTISIG_SIG_NUMBER = 2;

‎modules/sdk-coin-stx/src/lib/contractBuilder.ts‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import { CONTRACT_NAME_SENDMANY, VALID_STAKING_CONTRACT_NAMES } from './constant
2222
import { AbstractContractBuilder } from './abstractContractBuilder';
2323

2424
export class ContractBuilder extends AbstractContractBuilder {
25+
private static readonly CLARITY_TYPE_VALUES = new Set<string>(Object.values(ClarityType));
26+
/** Clarity values that carry no `value` property in v7. */
27+
private static readonly VALUELESS_CLARITY_TYPES = new Set<string>([
28+
ClarityType.OptionalNone,
29+
ClarityType.BoolTrue,
30+
ClarityType.BoolFalse,
31+
]);
32+
2533
constructor(_coinConfig: Readonly<CoinConfig>) {
2634
super(_coinConfig);
2735
}
@@ -92,16 +100,35 @@ export class ContractBuilder extends AbstractContractBuilder {
92100

93101
functionArgs(args: ClarityValueJson[] | ClarityValue[]): this {
94102
this._functionArgs = args.map((arg) => {
95-
if (!ClarityType[arg.type]) {
96-
return this.parseCv(arg);
97-
} else {
103+
if (this.isClarityValue(arg)) {
98104
// got direct clarity value after deserialization in fromImplementation
99105
return arg;
100106
}
107+
return this.parseCv(arg);
101108
});
102109
return this;
103110
}
104111

112+
/**
113+
* Positive shape check: a fully-formed `ClarityValue` vs a JSON `ClarityValueJson` descriptor.
114+
*
115+
* In `@stacks/transactions` v7 the `ClarityType` values are lower-case strings (`'uint'`,
116+
* `'contract'`, `'address'`, ...). A raw clarity value carries a `type` that is one of those values
117+
* AND a `value` property (except the valueless `none` / `true` / `false`), and never a `val` field.
118+
* JSON descriptors use `type` names (e.g. `optional`, `list`, `response`, `tuple`, `buffer`,
119+
* `contractPrincipal`) and always carry `val` — a malformed descriptor like `{ type: 'list' }`
120+
* therefore falls through to `parseCv` so its validation errors still surface.
121+
*/
122+
private isClarityValue(arg: ClarityValueJson | ClarityValue): arg is ClarityValue {
123+
if (typeof arg.type !== 'string' || !ContractBuilder.CLARITY_TYPE_VALUES.has(arg.type)) {
124+
return false;
125+
}
126+
if ('val' in arg) {
127+
return false;
128+
}
129+
return 'value' in arg || ContractBuilder.VALUELESS_CLARITY_TYPES.has(arg.type);
130+
}
131+
105132
private parseCv(arg: ClarityValueJson): ClarityValue {
106133
switch (arg.type) {
107134
case 'optional':

‎modules/sdk-coin-stx/src/lib/fungibleTokenTransferBuilder.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
AddressVersion,
66
ClarityValue,
77
ContractCallPayload,
8-
FungibleConditionCode,
9-
makeStandardFungiblePostCondition,
8+
Pc,
109
PostCondition,
1110
PostConditionMode,
1211
} from '@stacks/transactions';
@@ -141,25 +140,29 @@ export class FungibleTokenTransferBuilder extends AbstractContractBuilder {
141140
private tokenTransferParamsToPostCondition(tokenTransferParams: TokenTransferParams): PostCondition[] {
142141
if (!this._tokenName) {
143142
const contractTokenName = findContractTokenNameUsingContract(this._contractAddress, this._contractName);
144-
if (contractTokenName) {
145-
this.tokenName(contractTokenName);
143+
if (!contractTokenName) {
144+
// Fail the build rather than serialising an `undefined` asset name: such a post-condition can
145+
// never match, so a `Deny`-mode tx would only abort on-chain and burn the fee.
146+
throw new InvalidParameterValueError(
147+
`unknown fungible token contract: ${this._contractAddress}.${this._contractName}`
148+
);
146149
}
150+
this.tokenName(contractTokenName);
147151
}
148152
const amount: BigNum = new BigNum(tokenTransferParams.amount);
149153
return [
150-
makeStandardFungiblePostCondition(
154+
Pc.principal(
151155
getSTXAddressFromPubKeys(
152156
this._fromPubKeys,
153157
this._coinConfig.network.type === NetworkType.MAINNET
154158
? AddressVersion.MainnetMultiSig
155159
: AddressVersion.TestnetMultiSig,
156-
this._fromPubKeys.length > 1 ? AddressHashMode.SerializeP2SH : AddressHashMode.SerializeP2PKH,
160+
this._fromPubKeys.length > 1 ? AddressHashMode.P2SH : AddressHashMode.P2PKH,
157161
this._numberSignatures
158-
).address,
159-
FungibleConditionCode.Equal,
160-
amount.toString(),
161-
`${this._contractAddress}.${this._contractName}::${this._tokenName}`
162-
),
162+
).address
163+
)
164+
.willSendEq(amount.toString())
165+
.ft(`${this._contractAddress}.${this._contractName}`, this._tokenName),
163166
];
164167
}
165168
}

‎modules/sdk-coin-stx/src/lib/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export * from './sbtcWithdrawBuilder';
77
export * from './btcAddressUtils';
88
export * from './pox5Builder';
99
export * from './iface';
10+
export * from './postConditions';
1011
export * as Utils from './utils';

‎modules/sdk-coin-stx/src/lib/keyPair.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { randomBytes } from 'crypto';
22
import { bip32, ECPair } from '@bitgo/secp256k1';
3-
import { getAddressFromPublicKey, TransactionVersion } from '@stacks/transactions';
3+
import { getAddressFromPublicKey } from '@stacks/transactions';
4+
import { TransactionVersion } from '@stacks/network';
45
import {
56
DefaultKeys,
67
isPrivateKey,
@@ -123,6 +124,9 @@ export class KeyPair extends Secp256k1ExtendedKeyPair {
123124
* @returns {string} The public address
124125
*/
125126
getSTXAddress(compressed = false, network: TransactionVersion = TransactionVersion.Mainnet): string {
126-
return getAddressFromPublicKey(this.getKeys(compressed).pub, network);
127+
// v7 `getAddressFromPublicKey` takes a network name; `TransactionVersion` only has Mainnet/Testnet,
128+
// so anything other than Testnet maps to mainnet (there is no third value).
129+
const networkName = network === TransactionVersion.Testnet ? 'testnet' : 'mainnet';
130+
return getAddressFromPublicKey(this.getKeys(compressed).pub, networkName);
127131
}
128132
}

‎modules/sdk-coin-stx/src/lib/network.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BaseCoin as CoinConfig, NetworkType, StacksNetwork as StaticsStacksNetwork } from '@bitgo/statics';
2-
import { StacksMainnet, StacksNetwork, StacksTestnet } from '@stacks/network';
2+
import { STACKS_MAINNET, STACKS_TESTNET, StacksNetwork } from '@stacks/network';
33

44
/**
55
* Build the `@stacks/network` object a transaction is serialized against.
@@ -15,13 +15,14 @@ import { StacksMainnet, StacksNetwork, StacksTestnet } from '@stacks/network';
1515
*/
1616
export function toStacksNetwork(coinConfig: Readonly<CoinConfig>): StacksNetwork {
1717
const staticsNetwork = coinConfig.network as StaticsStacksNetwork;
18-
const network = staticsNetwork.type === NetworkType.MAINNET ? new StacksMainnet() : new StacksTestnet();
18+
const stock = staticsNetwork.type === NetworkType.MAINNET ? STACKS_MAINNET : STACKS_TESTNET;
19+
const network: StacksNetwork = { ...stock };
1920

2021
if (staticsNetwork.chainId !== undefined) {
2122
network.chainId = staticsNetwork.chainId;
2223
}
2324
if (staticsNetwork.transactionVersion !== undefined) {
24-
network.version = staticsNetwork.transactionVersion;
25+
network.transactionVersion = staticsNetwork.transactionVersion;
2526
}
2627

2728
return network;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { FungibleConditionCode, Pc, StakingPostCondition } from '@stacks/transactions';
2+
3+
/**
4+
* Comparator accepted by {@link makeStakingPostCondition}: either the `FungibleConditionCode` enum
5+
* from `@stacks/transactions` (`Equal`, `Greater`, `GreaterEqual`, `Less`, `LessEqual`) or the string
6+
* form used by the v7 wire format (`'eq' | 'gt' | 'gte' | 'lt' | 'lte'`).
7+
*/
8+
export type PostConditionCode = FungibleConditionCode | 'eq' | 'gt' | 'gte' | 'lt' | 'lte';
9+
10+
/** Amount accepted by {@link makeStakingPostCondition} (micro-STX). */
11+
export type PostConditionAmount = string | number | bigint;
12+
13+
type Comparator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte';
14+
15+
function toComparator(code: PostConditionCode): Comparator {
16+
if (typeof code === 'string') {
17+
return code;
18+
}
19+
switch (code) {
20+
case FungibleConditionCode.Equal:
21+
return 'eq';
22+
case FungibleConditionCode.Greater:
23+
return 'gt';
24+
case FungibleConditionCode.GreaterEqual:
25+
return 'gte';
26+
case FungibleConditionCode.Less:
27+
return 'lt';
28+
case FungibleConditionCode.LessEqual:
29+
return 'lte';
30+
default:
31+
throw new Error(`Unsupported fungible condition code: ${code}`);
32+
}
33+
}
34+
35+
function applyComparator(
36+
builder: ReturnType<typeof Pc.principal>,
37+
code: PostConditionCode,
38+
amount: PostConditionAmount
39+
): ReturnType<ReturnType<typeof Pc.principal>['willSendEq']> {
40+
switch (toComparator(code)) {
41+
case 'eq':
42+
return builder.willSendEq(amount);
43+
case 'gt':
44+
return builder.willSendGt(amount);
45+
case 'gte':
46+
return builder.willSendGte(amount);
47+
case 'lt':
48+
return builder.willSendLt(amount);
49+
case 'lte':
50+
return builder.willSendLte(amount);
51+
}
52+
}
53+
54+
/**
55+
* Build a SIP-044 **staking** post-condition (`PostConditionType.Staking = 3`).
56+
*
57+
* This is the post-condition required by PoX-5 `register-for-bond` (and any call that locks the
58+
* staker's STX): the staked STX is tracked under the staking condition type, so an STX post-condition
59+
* does **not** cover it and a `Deny`-mode tx with no/mismatched post-condition aborts on-chain with
60+
* `… STX was staked by … but not checked` (SI-1936).
61+
*
62+
* Only `@stacks/transactions` v7 can construct it — the driver for the v7 migration (SI-2024).
63+
*
64+
* @param address the staker principal whose locked STX is asserted
65+
* @param code the comparator (`'eq' | 'gt' | 'gte' | 'lt' | 'lte'` or `FungibleConditionCode`)
66+
* @param amount the locked amount in micro-STX
67+
*
68+
* @example
69+
* ```ts
70+
* builder.postConditionMode(PostConditionMode.Deny);
71+
* builder.postConditions([makeStakingPostCondition(staker, 'eq', amountUstx)]);
72+
* ```
73+
*/
74+
export function makeStakingPostCondition(
75+
address: string,
76+
code: PostConditionCode,
77+
amount: PostConditionAmount
78+
): StakingPostCondition {
79+
return applyComparator(Pc.principal(address), code, amount).ustxToLock();
80+
}

‎modules/sdk-coin-stx/src/lib/sbtcWithdrawBuilder.ts‎

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import {
66
AddressVersion,
77
bufferCV,
88
ClarityType,
9-
createAssetInfo,
10-
FungibleConditionCode,
11-
makeStandardFungiblePostCondition,
9+
ContractCallPayload,
10+
Pc,
1211
PostCondition,
1312
PostConditionMode,
1413
tupleCV,
@@ -19,7 +18,6 @@ import { Transaction } from './transaction';
1918
import { getSTXAddressFromPubKeys, isValidAmount } from './utils';
2019
import { SbtcWithdrawParams } from './iface';
2120
import { CONTRACT_NAME_SBTC_WITHDRAWAL, FUNCTION_NAME_INITIATE_WITHDRAWAL } from './constants';
22-
import { ContractCallPayload } from '@stacks/transactions/dist/payload';
2321
import { AbstractContractBuilder } from './abstractContractBuilder';
2422
import { decodeBtcAddress, isValidBtcAddress } from './btcAddressUtils';
2523

@@ -85,8 +83,8 @@ export class SbtcWithdrawBuilder extends AbstractContractBuilder {
8583
if (args[1].type !== ClarityType.Tuple) {
8684
throw new BuildTransactionError('Expected tuple for recipient argument');
8785
}
88-
const versionBuf = args[1].data['version'];
89-
const hashbytesBuf = args[1].data['hashbytes'];
86+
const versionBuf = args[1].value['version'];
87+
const hashbytesBuf = args[1].value['hashbytes'];
9088
if (versionBuf?.type !== ClarityType.Buffer || hashbytesBuf?.type !== ClarityType.Buffer) {
9189
throw new BuildTransactionError('Expected buffer fields in recipient tuple');
9290
}
@@ -121,16 +119,16 @@ export class SbtcWithdrawBuilder extends AbstractContractBuilder {
121119
if (recipientTuple?.type !== ClarityType.Tuple) {
122120
return undefined;
123121
}
124-
const versionBuf = recipientTuple.data['version'];
125-
const hashbytesBuf = recipientTuple.data['hashbytes'];
122+
const versionBuf = recipientTuple.value['version'];
123+
const hashbytesBuf = recipientTuple.value['hashbytes'];
126124
if (versionBuf?.type !== ClarityType.Buffer || hashbytesBuf?.type !== ClarityType.Buffer) {
127125
return undefined;
128126
}
129127
return {
130128
amount: this._withdrawParams.amount,
131129
maxFee: this._withdrawParams.maxFee,
132-
recipientVersion: versionBuf.buffer[0],
133-
recipientHashBytes: Buffer.from(hashbytesBuf.buffer),
130+
recipientVersion: Buffer.from(versionBuf.value, 'hex')[0],
131+
recipientHashBytes: Buffer.from(hashbytesBuf.value, 'hex'),
134132
};
135133
}
136134

@@ -174,19 +172,18 @@ export class SbtcWithdrawBuilder extends AbstractContractBuilder {
174172
const sbtcContractAddress = network.sbtcWithdrawalContractAddress;
175173

176174
return [
177-
makeStandardFungiblePostCondition(
175+
Pc.principal(
178176
getSTXAddressFromPubKeys(
179177
this._fromPubKeys,
180178
this._coinConfig.network.type === NetworkType.MAINNET
181179
? AddressVersion.MainnetMultiSig
182180
: AddressVersion.TestnetMultiSig,
183-
this._fromPubKeys.length > 1 ? AddressHashMode.SerializeP2SH : AddressHashMode.SerializeP2PKH,
181+
this._fromPubKeys.length > 1 ? AddressHashMode.P2SH : AddressHashMode.P2PKH,
184182
this._numberSignatures
185-
).address,
186-
FungibleConditionCode.Equal,
187-
amount.toString(),
188-
createAssetInfo(sbtcContractAddress, SBTC_TOKEN_CONTRACT_NAME, SBTC_TOKEN_ASSET_NAME)
189-
),
183+
).address
184+
)
185+
.willSendEq(amount.toString())
186+
.ft(`${sbtcContractAddress}.${SBTC_TOKEN_CONTRACT_NAME}`, SBTC_TOKEN_ASSET_NAME),
190187
];
191188
}
192189
}

0 commit comments

Comments
 (0)