Skip to content
Draft
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
1 change: 1 addition & 0 deletions modules/abstract-cosmos/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface CosmosLikeTransaction<CustomMessage = never> {
readonly signature?: Uint8Array;
readonly hash?: string;
readonly memo?: string;
readonly execType?: string;
}

export interface KeyShares {
Expand Down
12 changes: 10 additions & 2 deletions modules/abstract-cosmos/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export abstract class CosmosTransactionBuilder<CustomMessage = never> extends Ba
protected _memo?: string;

protected _utils: CosmosUtils<CustomMessage>;
protected _execType?: string;

constructor(_coinConfig: Readonly<CoinConfig>, _utils: CosmosUtils<CustomMessage>) {
super(_coinConfig);
Expand Down Expand Up @@ -158,6 +159,11 @@ export abstract class CosmosTransactionBuilder<CustomMessage = never> extends Ba
return this;
}

execType(value: string): this {
this._execType = value;
return this;
}

/**
* Initialize the transaction builder fields using the decoded transaction data
* @param {CosmosTransaction} tx the transaction data
Expand Down Expand Up @@ -214,7 +220,8 @@ export abstract class CosmosTransactionBuilder<CustomMessage = never> extends Ba
this._messages,
this._gasBudget,
this._publicKey,
this._memo
this._memo,
this._execType
);

const privateKey = this._signer?.getPrivateKey();
Expand All @@ -238,7 +245,8 @@ export abstract class CosmosTransactionBuilder<CustomMessage = never> extends Ba
this._gasBudget,
this._publicKey,
this._signature,
this._memo
this._memo,
this._execType
);
}
this.transaction.loadInputsAndOutputs();
Expand Down
12 changes: 9 additions & 3 deletions modules/abstract-cosmos/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ export class CosmosUtils<CustomMessage = never> implements BaseUtils {
// For group proposal/vote messages, the pre-encoded bytes contain the full message
try {
const decoded = this.registry.decode({ typeUrl: msg.typeUrl, value: msg.value });
if (cosmosLikeTransaction.execType !== undefined && msg.typeUrl === constants.groupProposalMsgTypeUrl) {
decoded.exec = cosmosLikeTransaction.execType === 'EXEC_TRY' ? 1 : 0;
}
return {
typeUrl: msg.typeUrl,
value: decoded,
Expand Down Expand Up @@ -737,14 +740,16 @@ export class CosmosUtils<CustomMessage = never> implements BaseUtils {
messages: MessageData<CustomMessage>[],
gasBudget: FeeData,
publicKey?: string,
memo?: string
memo?: string,
execType?: string
): CosmosLikeTransaction<CustomMessage> {
const cosmosLikeTxn = {
sequence: sequence,
sendMessages: messages,
gasBudget: gasBudget,
publicKey: publicKey,
memo: memo,
execType: execType,
};
this.validateTransaction(cosmosLikeTxn);
return cosmosLikeTxn;
Expand All @@ -766,9 +771,10 @@ export class CosmosUtils<CustomMessage = never> implements BaseUtils {
gasBudget: FeeData,
publicKey?: string,
signature?: Buffer,
memo?: string
memo?: string,
execType?: string
): CosmosLikeTransaction<CustomMessage> {
const cosmosLikeTxn = this.createTransaction(sequence, messages, gasBudget, publicKey, memo);
const cosmosLikeTxn = this.createTransaction(sequence, messages, gasBudget, publicKey, memo, execType);
let hash = constants.UNAVAILABLE_TEXT;
if (signature !== undefined) {
const unsignedTx = this.createTxRawFromCosmosLikeTransaction(cosmosLikeTxn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import should from 'should';

import { BitGoAPI } from '@bitgo/sdk-api';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { fromBase64, toHex } from '@cosmjs/encoding';
import { Hash, Thash } from '../../../src';
import * as testData from '../../resources/hash';

Expand Down Expand Up @@ -111,4 +112,90 @@ describe('Hash Transaction Builder', async () => {
);
}
});

/**
* Tests for the execType setter on CosmosTransactionBuilder.
* execType is used with MsgSubmitProposal group proposals to control how the proposal
* is executed — EXEC_TRY (exec=1) causes immediate execution, EXEC_UNSPECIFIED (exec=0) does not.
*
* To avoid message-validation side-effects from the proposal-decoding path in messages(),
* we use the pre-encoded MessageData route: factory.from() deserializes an existing
* MsgSubmitProposal tx, which stores sendMessages[0].value as a Uint8Array. That Uint8Array
* is then forwarded directly to a new ContractCallBuilder. When ContractCallBuilder.messages()
* receives an object without a `.msg` field it treats it as a pre-encoded MessageData and
* skips all address / proposal-type validation, keeping the test focused on execType behaviour.
*/
describe('execType tests', async () => {
/**
* Returns the pre-encoded MessageData extracted from a round-trip through factory.from()
* on the known TEST_SUBMIT_PROPOSAL tx. The deserialized sendMessages[0] has
* { typeUrl: '/cosmos.group.v1.MsgSubmitProposal', value: Uint8Array } which bypasses
* the isGroupProposal / validateExecuteContractMessage validation in ContractCallBuilder.
*/
const getPreEncodedProposalMessage = async () => {
const seedBuilder = factory.from(testData.TEST_SUBMIT_PROPOSAL.randomMsgSubmitProposalEncoded);
const seedTx = await seedBuilder.build();
// sendMessages[0].value is a Uint8Array after deserialization — the pre-encoded path
return seedTx.cosmosLikeTransaction.sendMessages[0];
};

/**
* Builds a ContractCallBuilder using the pre-encoded proposal MessageData and the
* provided execType string, using TEST_CONTRACT_CALL metadata for chain parameters.
*/
const buildProposalTxWithExecType = async (execTypeValue: string) => {
const preEncodedMessage = await getPreEncodedProposalMessage();

const txBuilder = factory.getContractCallBuilder();
txBuilder.sequence(testData.TEST_CONTRACT_CALL.sequence);
txBuilder.accountNumber(testData.TEST_CONTRACT_CALL.accountNumber);
txBuilder.chainId(testData.TEST_CONTRACT_CALL.chainId);
txBuilder.gasBudget({
amount: [{ denom: 'nhash', amount: testData.TEST_CONTRACT_CALL.fee }],
gasLimit: testData.TEST_CONTRACT_CALL.gasLimit,
});
txBuilder.publicKey(toHex(fromBase64(testData.TEST_CONTRACT_CALL.pubKey)));
// Passing the pre-encoded MessageData (value is Uint8Array) bypasses isGroupProposal
// detection and goes through the "pre-encoded round-trip" branch in messages()
txBuilder.messages([preEncodedMessage]);
txBuilder.execType(execTypeValue);
return txBuilder.build();
};

it('should store execType on cosmosLikeTransaction when execType is set to EXEC_UNSPECIFIED', async function () {
// Verifies that calling execType('EXEC_UNSPECIFIED') propagates the value through
// createTransaction() into cosmosLikeTransaction.execType, which utils then uses
// to set decoded.exec = 0 when serializing the group proposal message.
const tx = await buildProposalTxWithExecType('EXEC_UNSPECIFIED');
tx.cosmosLikeTransaction.execType.should.equal('EXEC_UNSPECIFIED');
});

it('should store execType on cosmosLikeTransaction when execType is set to EXEC_TRY', async function () {
// Verifies that EXEC_TRY is stored correctly — this is the value that causes
// getSendMessagesForEncodingTx to set decoded.exec = 1 on the group proposal message.
const tx = await buildProposalTxWithExecType('EXEC_TRY');
tx.cosmosLikeTransaction.execType.should.equal('EXEC_TRY');
});

it('should leave execType undefined when execType() is never called', async function () {
// Verifies that omitting the execType() call leaves cosmosLikeTransaction.execType
// as undefined, which means getSendMessagesForEncodingTx will not override the exec
// field and the original encoded value in the message bytes is preserved.
const preEncodedMessage = await getPreEncodedProposalMessage();

const txBuilder = factory.getContractCallBuilder();
txBuilder.sequence(testData.TEST_CONTRACT_CALL.sequence);
txBuilder.accountNumber(testData.TEST_CONTRACT_CALL.accountNumber);
txBuilder.chainId(testData.TEST_CONTRACT_CALL.chainId);
txBuilder.gasBudget({
amount: [{ denom: 'nhash', amount: testData.TEST_CONTRACT_CALL.fee }],
gasLimit: testData.TEST_CONTRACT_CALL.gasLimit,
});
txBuilder.publicKey(toHex(fromBase64(testData.TEST_CONTRACT_CALL.pubKey)));
txBuilder.messages([preEncodedMessage]);
// Intentionally not calling txBuilder.execType() to confirm the field remains undefined
const tx = await txBuilder.build();
should.equal(tx.cosmosLikeTransaction.execType, undefined);
});
});
});
Loading