Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions modules/sdk-coin-stx/src/lib/abstractContractBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,43 @@ export abstract class AbstractContractBuilder extends TransactionBuilder {
this._contractName = txData.payload.contractName;
this._functionName = txData.payload.functionName;
this._functionArgs = txData.payload.functionArgs;
// Restore post-conditions from the deserialized tx — `toJson()` does not carry them, so
// without this a re-build (e.g. the fee re-build via `factory.from(hex)`) would drop them and
// a `Deny`-mode contract call would abort on-chain (SIP-005 post-condition check).
if (tx.stxTransaction) {
this._postConditionMode = tx.stxTransaction.postConditionMode;
this._postConditions = tx.stxTransaction.postConditions.values as PostCondition[];
}
super.initBuilder(tx);
} else {
throw new BuildTransactionError('Transaction should be contract call');
}
}

/**
* Set the post-condition mode (`Allow` / `Deny`) for this contract call.
*
* @obligation callers building a contract call that moves assets MUST set a mode AND matching
* post-conditions, or leave this unset and let the caller attach explicit post-conditions —
* consequence: an unset mode defaults to `Deny` in `@stacks/transactions`, so any asset movement
* not covered by a post-condition aborts the tx on-chain
* @returns {AbstractContractBuilder} this builder
*/
postConditionMode(mode: PostConditionMode): this {
this._postConditionMode = mode;
return this;
}

/**
* Set the post-conditions for this contract call.
*
* @returns {AbstractContractBuilder} this builder
*/
postConditions(postConditions: PostCondition[]): this {
this._postConditions = postConditions;
return this;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this._options = this.buildContractCallOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
cvToString,
cvToValue,
deserializeCV,
FungibleConditionCode,
makeStandardSTXPostCondition,
PostConditionMode,
} from '@stacks/transactions';
import { coins } from '@bitgo/statics';
import should from 'should';
Expand Down Expand Up @@ -115,6 +118,34 @@ describe('Stacks: PoX-5 Builder', function () {
should.equal((await rebuilt.build()).toBroadcastFormat(), tx.toBroadcastFormat());
});

it('carries post-condition mode/conditions and preserves them across the fee re-build (SI-1936)', async () => {
const functionArgs = pox5RegisterForBondArgsHex.map((hex) =>
deserializeCV(Buffer.from(hex.replace(/^0x/, ''), 'hex'))
);
const postCondition = makeStandardSTXPostCondition(
testData.TX_SENDER.address,
FungibleConditionCode.LessEqual,
100000000
);
const builder = configure(factory.getPox5Builder());
builder.functionName('register-for-bond');
builder.functionArgs(functionArgs);
builder.postConditionMode(PostConditionMode.Deny);
builder.postConditions([postCondition]);
const tx = (await builder.build()) as StxLib.Transaction;

should.equal(tx.stxTransaction.postConditionMode, PostConditionMode.Deny);
should.equal(tx.stxTransaction.postConditions.values.length, 1);

// `buildTransactionWithFee` re-builds via `factory.from(hex)`; the post-conditions must survive
// (initBuilder restores them from the deserialized tx).
const rebuilt = factory.from(tx.toBroadcastFormat());
rebuilt.fromPubKey(testData.TX_SENDER.pub);
const rebuiltTx = (await rebuilt.build()) as StxLib.Transaction;
should.equal(rebuiltTx.stxTransaction.postConditionMode, PostConditionMode.Deny);
should.equal(rebuiltTx.stxTransaction.postConditions.values.length, 1);
});

it('validates the PoX-5 contract address during factory routing', async () => {
const builder = configure(factory.getPox5Builder());
builder.registerForBond({
Expand Down
Loading