From 9eab89fcdeffd48780a99dec4c9b1af2ba205137 Mon Sep 17 00:00:00 2001 From: Sreeraj S Date: Tue, 3 Feb 2026 20:14:14 +0530 Subject: [PATCH 1/2] feat(abstract-cosmos,sdk-coin-hash): support group proposals Adding support for Figure Markets to integrate bitgo wallets to their tokenized equity exchange - Add support for base64-encoded pre-encoded messages in ContractCallBuilder - Add group proposal message support (MsgSubmitProposal) - Add ProposalCompiled protobuf definitions - Add test utilities for group proposal wrapping - Add ContractCallBuilder tests with pre-encoded messages The exchange module innermsg is wrapped inside a GroupProposal, hence currently we do not fully support decoding the messages to identify what asset is being transfered. This will be added in followup PRs. This is to exhit bitgo's ability to sign contract interactions Ticket-WIN-8842 --- .../resources/ProposalCompiled.d.ts | 263 +++++++ .../resources/ProposalCompiled.js | 696 ++++++++++++++++++ .../src/lib/ContractCallBuilder.ts | 26 +- modules/abstract-cosmos/src/lib/constants.ts | 1 + modules/abstract-cosmos/src/lib/iface.ts | 2 +- .../abstract-cosmos/src/lib/transaction.ts | 21 + .../src/lib/transactionBuilder.ts | 16 +- modules/abstract-cosmos/src/lib/utils.ts | 67 +- modules/sdk-coin-hash/test/resources/hash.ts | 19 + .../transactionBuilder/contractCallBuilder.ts | 100 +++ .../test/utils/groupProposalHelper.ts | 33 + 11 files changed, 1223 insertions(+), 21 deletions(-) create mode 100644 modules/abstract-cosmos/resources/ProposalCompiled.d.ts create mode 100644 modules/abstract-cosmos/resources/ProposalCompiled.js create mode 100644 modules/sdk-coin-hash/test/unit/transactionBuilder/contractCallBuilder.ts create mode 100644 modules/sdk-coin-hash/test/utils/groupProposalHelper.ts diff --git a/modules/abstract-cosmos/resources/ProposalCompiled.d.ts b/modules/abstract-cosmos/resources/ProposalCompiled.d.ts new file mode 100644 index 0000000000..e2c91492b5 --- /dev/null +++ b/modules/abstract-cosmos/resources/ProposalCompiled.d.ts @@ -0,0 +1,263 @@ +import * as $protobuf from 'protobufjs'; +import Long = require('long'); +/** Namespace cosmos. */ +export namespace cosmos { + /** Namespace group. */ + namespace group { + /** Namespace v1. */ + namespace v1 { + /** Properties of a MsgSubmitProposal. */ + interface IMsgSubmitProposal { + /** MsgSubmitProposal groupPolicyAddress */ + groupPolicyAddress?: string | null; + + /** MsgSubmitProposal proposers */ + proposers?: string[] | null; + + /** MsgSubmitProposal metadata */ + metadata?: string | null; + + /** MsgSubmitProposal messages */ + messages?: google.protobuf.IAny[] | null; + + /** MsgSubmitProposal exec */ + exec?: cosmos.group.v1.Exec | null; + + /** MsgSubmitProposal title */ + title?: string | null; + + /** MsgSubmitProposal summary */ + summary?: string | null; + } + + /** Represents a MsgSubmitProposal. */ + class MsgSubmitProposal implements IMsgSubmitProposal { + /** + * Constructs a new MsgSubmitProposal. + * @param [properties] Properties to set + */ + constructor(properties?: cosmos.group.v1.IMsgSubmitProposal); + + /** MsgSubmitProposal groupPolicyAddress. */ + public groupPolicyAddress: string; + + /** MsgSubmitProposal proposers. */ + public proposers: string[]; + + /** MsgSubmitProposal metadata. */ + public metadata: string; + + /** MsgSubmitProposal messages. */ + public messages: google.protobuf.IAny[]; + + /** MsgSubmitProposal exec. */ + public exec: cosmos.group.v1.Exec; + + /** MsgSubmitProposal title. */ + public title: string; + + /** MsgSubmitProposal summary. */ + public summary: string; + + /** + * Creates a new MsgSubmitProposal instance using the specified properties. + * @param [properties] Properties to set + * @returns MsgSubmitProposal instance + */ + public static create(properties?: cosmos.group.v1.IMsgSubmitProposal): cosmos.group.v1.MsgSubmitProposal; + + /** + * Encodes the specified MsgSubmitProposal message. Does not implicitly {@link cosmos.group.v1.MsgSubmitProposal.verify|verify} messages. + * @param message MsgSubmitProposal message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: cosmos.group.v1.IMsgSubmitProposal, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified MsgSubmitProposal message, length delimited. Does not implicitly {@link cosmos.group.v1.MsgSubmitProposal.verify|verify} messages. + * @param message MsgSubmitProposal message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited( + message: cosmos.group.v1.IMsgSubmitProposal, + writer?: $protobuf.Writer + ): $protobuf.Writer; + + /** + * Decodes a MsgSubmitProposal message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns MsgSubmitProposal + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: $protobuf.Reader | Uint8Array, length?: number): cosmos.group.v1.MsgSubmitProposal; + + /** + * Decodes a MsgSubmitProposal message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns MsgSubmitProposal + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: $protobuf.Reader | Uint8Array): cosmos.group.v1.MsgSubmitProposal; + + /** + * Verifies a MsgSubmitProposal message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): string | null; + + /** + * Creates a MsgSubmitProposal message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns MsgSubmitProposal + */ + public static fromObject(object: { [k: string]: any }): cosmos.group.v1.MsgSubmitProposal; + + /** + * Creates a plain object from a MsgSubmitProposal message. Also converts values to other types if specified. + * @param message MsgSubmitProposal + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject( + message: cosmos.group.v1.MsgSubmitProposal, + options?: $protobuf.IConversionOptions + ): { [k: string]: any }; + + /** + * Converts this MsgSubmitProposal to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for MsgSubmitProposal + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Exec enum. */ + enum Exec { + EXEC_UNSPECIFIED = 0, + EXEC_TRY = 1, + } + } + } +} + +/** Namespace google. */ +export namespace google { + /** Namespace protobuf. */ + namespace protobuf { + /** Properties of an Any. */ + interface IAny { + /** Any type_url */ + type_url?: string | null; + + /** Any value */ + value?: Uint8Array | null; + } + + /** Represents an Any. */ + class Any implements IAny { + /** + * Constructs a new Any. + * @param [properties] Properties to set + */ + constructor(properties?: google.protobuf.IAny); + + /** Any type_url. */ + public type_url: string; + + /** Any value. */ + public value: Uint8Array; + + /** + * Creates a new Any instance using the specified properties. + * @param [properties] Properties to set + * @returns Any instance + */ + public static create(properties?: google.protobuf.IAny): google.protobuf.Any; + + /** + * Encodes the specified Any message. Does not implicitly {@link google.protobuf.Any.verify|verify} messages. + * @param message Any message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: google.protobuf.IAny, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified Any message, length delimited. Does not implicitly {@link google.protobuf.Any.verify|verify} messages. + * @param message Any message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: google.protobuf.IAny, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes an Any message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns Any + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: $protobuf.Reader | Uint8Array, length?: number): google.protobuf.Any; + + /** + * Decodes an Any message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns Any + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: $protobuf.Reader | Uint8Array): google.protobuf.Any; + + /** + * Verifies an Any message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): string | null; + + /** + * Creates an Any message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns Any + */ + public static fromObject(object: { [k: string]: any }): google.protobuf.Any; + + /** + * Creates a plain object from an Any message. Also converts values to other types if specified. + * @param message Any + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject( + message: google.protobuf.Any, + options?: $protobuf.IConversionOptions + ): { [k: string]: any }; + + /** + * Converts this Any to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for Any + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + } +} diff --git a/modules/abstract-cosmos/resources/ProposalCompiled.js b/modules/abstract-cosmos/resources/ProposalCompiled.js new file mode 100644 index 0000000000..f8302f8c1d --- /dev/null +++ b/modules/abstract-cosmos/resources/ProposalCompiled.js @@ -0,0 +1,696 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +'use strict'; + +var $protobuf = require('protobufjs/minimal'); + +// Common aliases +var $Reader = $protobuf.Reader, + $Writer = $protobuf.Writer, + $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots['default'] || ($protobuf.roots['default'] = {}); + +$root.cosmos = (function () { + /** + * Namespace cosmos. + * @exports cosmos + * @namespace + */ + var cosmos = {}; + + cosmos.group = (function () { + /** + * Namespace group. + * @memberof cosmos + * @namespace + */ + var group = {}; + + group.v1 = (function () { + /** + * Namespace v1. + * @memberof cosmos.group + * @namespace + */ + var v1 = {}; + + v1.MsgSubmitProposal = (function () { + /** + * Properties of a MsgSubmitProposal. + * @memberof cosmos.group.v1 + * @interface IMsgSubmitProposal + * @property {string|null} [groupPolicyAddress] MsgSubmitProposal groupPolicyAddress + * @property {Array.|null} [proposers] MsgSubmitProposal proposers + * @property {string|null} [metadata] MsgSubmitProposal metadata + * @property {Array.|null} [messages] MsgSubmitProposal messages + * @property {cosmos.group.v1.Exec|null} [exec] MsgSubmitProposal exec + * @property {string|null} [title] MsgSubmitProposal title + * @property {string|null} [summary] MsgSubmitProposal summary + */ + + /** + * Constructs a new MsgSubmitProposal. + * @memberof cosmos.group.v1 + * @classdesc Represents a MsgSubmitProposal. + * @implements IMsgSubmitProposal + * @constructor + * @param {cosmos.group.v1.IMsgSubmitProposal=} [properties] Properties to set + */ + function MsgSubmitProposal(properties) { + this.proposers = []; + this.messages = []; + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; + } + + /** + * MsgSubmitProposal groupPolicyAddress. + * @member {string} groupPolicyAddress + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.groupPolicyAddress = ''; + + /** + * MsgSubmitProposal proposers. + * @member {Array.} proposers + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.proposers = $util.emptyArray; + + /** + * MsgSubmitProposal metadata. + * @member {string} metadata + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.metadata = ''; + + /** + * MsgSubmitProposal messages. + * @member {Array.} messages + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.messages = $util.emptyArray; + + /** + * MsgSubmitProposal exec. + * @member {cosmos.group.v1.Exec} exec + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.exec = 0; + + /** + * MsgSubmitProposal title. + * @member {string} title + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.title = ''; + + /** + * MsgSubmitProposal summary. + * @member {string} summary + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + */ + MsgSubmitProposal.prototype.summary = ''; + + /** + * Creates a new MsgSubmitProposal instance using the specified properties. + * @function create + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {cosmos.group.v1.IMsgSubmitProposal=} [properties] Properties to set + * @returns {cosmos.group.v1.MsgSubmitProposal} MsgSubmitProposal instance + */ + MsgSubmitProposal.create = function create(properties) { + return new MsgSubmitProposal(properties); + }; + + /** + * Encodes the specified MsgSubmitProposal message. Does not implicitly {@link cosmos.group.v1.MsgSubmitProposal.verify|verify} messages. + * @function encode + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {cosmos.group.v1.IMsgSubmitProposal} message MsgSubmitProposal message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MsgSubmitProposal.encode = function encode(message, writer) { + if (!writer) writer = $Writer.create(); + if (message.groupPolicyAddress != null && Object.hasOwnProperty.call(message, 'groupPolicyAddress')) + writer.uint32(/* id 1, wireType 2 =*/ 10).string(message.groupPolicyAddress); + if (message.proposers != null && message.proposers.length) + for (var i = 0; i < message.proposers.length; ++i) + writer.uint32(/* id 2, wireType 2 =*/ 18).string(message.proposers[i]); + if (message.metadata != null && Object.hasOwnProperty.call(message, 'metadata')) + writer.uint32(/* id 3, wireType 2 =*/ 26).string(message.metadata); + if (message.messages != null && message.messages.length) + for (var i = 0; i < message.messages.length; ++i) + $root.google.protobuf.Any.encode( + message.messages[i], + writer.uint32(/* id 4, wireType 2 =*/ 34).fork() + ).ldelim(); + if (message.exec != null && Object.hasOwnProperty.call(message, 'exec')) + writer.uint32(/* id 5, wireType 0 =*/ 40).int32(message.exec); + if (message.title != null && Object.hasOwnProperty.call(message, 'title')) + writer.uint32(/* id 6, wireType 2 =*/ 50).string(message.title); + if (message.summary != null && Object.hasOwnProperty.call(message, 'summary')) + writer.uint32(/* id 7, wireType 2 =*/ 58).string(message.summary); + return writer; + }; + + /** + * Encodes the specified MsgSubmitProposal message, length delimited. Does not implicitly {@link cosmos.group.v1.MsgSubmitProposal.verify|verify} messages. + * @function encodeDelimited + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {cosmos.group.v1.IMsgSubmitProposal} message MsgSubmitProposal message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MsgSubmitProposal.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a MsgSubmitProposal message from the specified reader or buffer. + * @function decode + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {cosmos.group.v1.MsgSubmitProposal} MsgSubmitProposal + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MsgSubmitProposal.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, + message = new $root.cosmos.group.v1.MsgSubmitProposal(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) break; + switch (tag >>> 3) { + case 1: { + message.groupPolicyAddress = reader.string(); + break; + } + case 2: { + if (!(message.proposers && message.proposers.length)) message.proposers = []; + message.proposers.push(reader.string()); + break; + } + case 3: { + message.metadata = reader.string(); + break; + } + case 4: { + if (!(message.messages && message.messages.length)) message.messages = []; + message.messages.push($root.google.protobuf.Any.decode(reader, reader.uint32())); + break; + } + case 5: { + message.exec = reader.int32(); + break; + } + case 6: { + message.title = reader.string(); + break; + } + case 7: { + message.summary = reader.string(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a MsgSubmitProposal message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {cosmos.group.v1.MsgSubmitProposal} MsgSubmitProposal + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MsgSubmitProposal.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a MsgSubmitProposal message. + * @function verify + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + MsgSubmitProposal.verify = function verify(message) { + if (typeof message !== 'object' || message === null) return 'object expected'; + if (message.groupPolicyAddress != null && message.hasOwnProperty('groupPolicyAddress')) + if (!$util.isString(message.groupPolicyAddress)) return 'groupPolicyAddress: string expected'; + if (message.proposers != null && message.hasOwnProperty('proposers')) { + if (!Array.isArray(message.proposers)) return 'proposers: array expected'; + for (var i = 0; i < message.proposers.length; ++i) + if (!$util.isString(message.proposers[i])) return 'proposers: string[] expected'; + } + if (message.metadata != null && message.hasOwnProperty('metadata')) + if (!$util.isString(message.metadata)) return 'metadata: string expected'; + if (message.messages != null && message.hasOwnProperty('messages')) { + if (!Array.isArray(message.messages)) return 'messages: array expected'; + for (var i = 0; i < message.messages.length; ++i) { + var error = $root.google.protobuf.Any.verify(message.messages[i]); + if (error) return 'messages.' + error; + } + } + if (message.exec != null && message.hasOwnProperty('exec')) + switch (message.exec) { + default: + return 'exec: enum value expected'; + case 0: + case 1: + break; + } + if (message.title != null && message.hasOwnProperty('title')) + if (!$util.isString(message.title)) return 'title: string expected'; + if (message.summary != null && message.hasOwnProperty('summary')) + if (!$util.isString(message.summary)) return 'summary: string expected'; + return null; + }; + + /** + * Creates a MsgSubmitProposal message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {Object.} object Plain object + * @returns {cosmos.group.v1.MsgSubmitProposal} MsgSubmitProposal + */ + MsgSubmitProposal.fromObject = function fromObject(object) { + if (object instanceof $root.cosmos.group.v1.MsgSubmitProposal) return object; + var message = new $root.cosmos.group.v1.MsgSubmitProposal(); + if (object.groupPolicyAddress != null) message.groupPolicyAddress = String(object.groupPolicyAddress); + if (object.proposers) { + if (!Array.isArray(object.proposers)) + throw TypeError('.cosmos.group.v1.MsgSubmitProposal.proposers: array expected'); + message.proposers = []; + for (var i = 0; i < object.proposers.length; ++i) message.proposers[i] = String(object.proposers[i]); + } + if (object.metadata != null) message.metadata = String(object.metadata); + if (object.messages) { + if (!Array.isArray(object.messages)) + throw TypeError('.cosmos.group.v1.MsgSubmitProposal.messages: array expected'); + message.messages = []; + for (var i = 0; i < object.messages.length; ++i) { + if (typeof object.messages[i] !== 'object') + throw TypeError('.cosmos.group.v1.MsgSubmitProposal.messages: object expected'); + message.messages[i] = $root.google.protobuf.Any.fromObject(object.messages[i]); + } + } + switch (object.exec) { + default: + if (typeof object.exec === 'number') { + message.exec = object.exec; + break; + } + break; + case 'EXEC_UNSPECIFIED': + case 0: + message.exec = 0; + break; + case 'EXEC_TRY': + case 1: + message.exec = 1; + break; + } + if (object.title != null) message.title = String(object.title); + if (object.summary != null) message.summary = String(object.summary); + return message; + }; + + /** + * Creates a plain object from a MsgSubmitProposal message. Also converts values to other types if specified. + * @function toObject + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {cosmos.group.v1.MsgSubmitProposal} message MsgSubmitProposal + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + MsgSubmitProposal.toObject = function toObject(message, options) { + if (!options) options = {}; + var object = {}; + if (options.arrays || options.defaults) { + object.proposers = []; + object.messages = []; + } + if (options.defaults) { + object.groupPolicyAddress = ''; + object.metadata = ''; + object.exec = options.enums === String ? 'EXEC_UNSPECIFIED' : 0; + object.title = ''; + object.summary = ''; + } + if (message.groupPolicyAddress != null && message.hasOwnProperty('groupPolicyAddress')) + object.groupPolicyAddress = message.groupPolicyAddress; + if (message.proposers && message.proposers.length) { + object.proposers = []; + for (var j = 0; j < message.proposers.length; ++j) object.proposers[j] = message.proposers[j]; + } + if (message.metadata != null && message.hasOwnProperty('metadata')) object.metadata = message.metadata; + if (message.messages && message.messages.length) { + object.messages = []; + for (var j = 0; j < message.messages.length; ++j) + object.messages[j] = $root.google.protobuf.Any.toObject(message.messages[j], options); + } + if (message.exec != null && message.hasOwnProperty('exec')) + object.exec = + options.enums === String + ? $root.cosmos.group.v1.Exec[message.exec] === undefined + ? message.exec + : $root.cosmos.group.v1.Exec[message.exec] + : message.exec; + if (message.title != null && message.hasOwnProperty('title')) object.title = message.title; + if (message.summary != null && message.hasOwnProperty('summary')) object.summary = message.summary; + return object; + }; + + /** + * Converts this MsgSubmitProposal to JSON. + * @function toJSON + * @memberof cosmos.group.v1.MsgSubmitProposal + * @instance + * @returns {Object.} JSON object + */ + MsgSubmitProposal.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for MsgSubmitProposal + * @function getTypeUrl + * @memberof cosmos.group.v1.MsgSubmitProposal + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + MsgSubmitProposal.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = 'type.googleapis.com'; + } + return typeUrlPrefix + '/cosmos.group.v1.MsgSubmitProposal'; + }; + + return MsgSubmitProposal; + })(); + + /** + * Exec enum. + * @name cosmos.group.v1.Exec + * @enum {number} + * @property {number} EXEC_UNSPECIFIED=0 EXEC_UNSPECIFIED value + * @property {number} EXEC_TRY=1 EXEC_TRY value + */ + v1.Exec = (function () { + var valuesById = {}, + values = Object.create(valuesById); + values[(valuesById[0] = 'EXEC_UNSPECIFIED')] = 0; + values[(valuesById[1] = 'EXEC_TRY')] = 1; + return values; + })(); + + return v1; + })(); + + return group; + })(); + + return cosmos; +})(); + +$root.google = (function () { + /** + * Namespace google. + * @exports google + * @namespace + */ + var google = {}; + + google.protobuf = (function () { + /** + * Namespace protobuf. + * @memberof google + * @namespace + */ + var protobuf = {}; + + protobuf.Any = (function () { + /** + * Properties of an Any. + * @memberof google.protobuf + * @interface IAny + * @property {string|null} [type_url] Any type_url + * @property {Uint8Array|null} [value] Any value + */ + + /** + * Constructs a new Any. + * @memberof google.protobuf + * @classdesc Represents an Any. + * @implements IAny + * @constructor + * @param {google.protobuf.IAny=} [properties] Properties to set + */ + function Any(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) this[keys[i]] = properties[keys[i]]; + } + + /** + * Any type_url. + * @member {string} type_url + * @memberof google.protobuf.Any + * @instance + */ + Any.prototype.type_url = ''; + + /** + * Any value. + * @member {Uint8Array} value + * @memberof google.protobuf.Any + * @instance + */ + Any.prototype.value = $util.newBuffer([]); + + /** + * Creates a new Any instance using the specified properties. + * @function create + * @memberof google.protobuf.Any + * @static + * @param {google.protobuf.IAny=} [properties] Properties to set + * @returns {google.protobuf.Any} Any instance + */ + Any.create = function create(properties) { + return new Any(properties); + }; + + /** + * Encodes the specified Any message. Does not implicitly {@link google.protobuf.Any.verify|verify} messages. + * @function encode + * @memberof google.protobuf.Any + * @static + * @param {google.protobuf.IAny} message Any message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + Any.encode = function encode(message, writer) { + if (!writer) writer = $Writer.create(); + if (message.type_url != null && Object.hasOwnProperty.call(message, 'type_url')) + writer.uint32(/* id 1, wireType 2 =*/ 10).string(message.type_url); + if (message.value != null && Object.hasOwnProperty.call(message, 'value')) + writer.uint32(/* id 2, wireType 2 =*/ 18).bytes(message.value); + return writer; + }; + + /** + * Encodes the specified Any message, length delimited. Does not implicitly {@link google.protobuf.Any.verify|verify} messages. + * @function encodeDelimited + * @memberof google.protobuf.Any + * @static + * @param {google.protobuf.IAny} message Any message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + Any.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes an Any message from the specified reader or buffer. + * @function decode + * @memberof google.protobuf.Any + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {google.protobuf.Any} Any + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + Any.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, + message = new $root.google.protobuf.Any(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) break; + switch (tag >>> 3) { + case 1: { + message.type_url = reader.string(); + break; + } + case 2: { + message.value = reader.bytes(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes an Any message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof google.protobuf.Any + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {google.protobuf.Any} Any + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + Any.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies an Any message. + * @function verify + * @memberof google.protobuf.Any + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + Any.verify = function verify(message) { + if (typeof message !== 'object' || message === null) return 'object expected'; + if (message.type_url != null && message.hasOwnProperty('type_url')) + if (!$util.isString(message.type_url)) return 'type_url: string expected'; + if (message.value != null && message.hasOwnProperty('value')) + if (!((message.value && typeof message.value.length === 'number') || $util.isString(message.value))) + return 'value: buffer expected'; + return null; + }; + + /** + * Creates an Any message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof google.protobuf.Any + * @static + * @param {Object.} object Plain object + * @returns {google.protobuf.Any} Any + */ + Any.fromObject = function fromObject(object) { + if (object instanceof $root.google.protobuf.Any) return object; + var message = new $root.google.protobuf.Any(); + if (object.type_url != null) message.type_url = String(object.type_url); + if (object.value != null) + if (typeof object.value === 'string') + $util.base64.decode(object.value, (message.value = $util.newBuffer($util.base64.length(object.value))), 0); + else if (object.value.length >= 0) message.value = object.value; + return message; + }; + + /** + * Creates a plain object from an Any message. Also converts values to other types if specified. + * @function toObject + * @memberof google.protobuf.Any + * @static + * @param {google.protobuf.Any} message Any + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + Any.toObject = function toObject(message, options) { + if (!options) options = {}; + var object = {}; + if (options.defaults) { + object.type_url = ''; + if (options.bytes === String) object.value = ''; + else { + object.value = []; + if (options.bytes !== Array) object.value = $util.newBuffer(object.value); + } + } + if (message.type_url != null && message.hasOwnProperty('type_url')) object.type_url = message.type_url; + if (message.value != null && message.hasOwnProperty('value')) + object.value = + options.bytes === String + ? $util.base64.encode(message.value, 0, message.value.length) + : options.bytes === Array + ? Array.prototype.slice.call(message.value) + : message.value; + return object; + }; + + /** + * Converts this Any to JSON. + * @function toJSON + * @memberof google.protobuf.Any + * @instance + * @returns {Object.} JSON object + */ + Any.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for Any + * @function getTypeUrl + * @memberof google.protobuf.Any + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + Any.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = 'type.googleapis.com'; + } + return typeUrlPrefix + '/google.protobuf.Any'; + }; + + return Any; + })(); + + return protobuf; + })(); + + return google; +})(); + +module.exports = $root; diff --git a/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts index 1fd775cf32..7791b16372 100644 --- a/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts +++ b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts @@ -1,8 +1,9 @@ import { TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { fromBase64 } from '@cosmjs/encoding'; import * as constants from './constants'; -import { ExecuteContractMessage } from './iface'; +import { CosmosTransactionMessage, ExecuteContractMessage, MessageData } from './iface'; import { CosmosTransactionBuilder } from './transactionBuilder'; import { CosmosUtils } from './utils'; @@ -19,8 +20,27 @@ export class ContractCallBuilder extends CosmosTransactio } /** @inheritdoc */ - messages(messages: ExecuteContractMessage[]): this { - this._messages = messages.map((executeContractMessage) => { + messages(messages: (CosmosTransactionMessage | MessageData)[]): this { + this._messages = messages.map((message) => { + const msg = message as MessageData; + const { typeUrl, value } = msg; + + // Handle pre-encoded messages (base64 string input) + if (typeUrl && typeof value === 'string') { + try { + return { typeUrl, value: fromBase64(value) } as MessageData; + } catch (err: unknown) { + throw new Error(`Invalid base64 string in message value: ${String(err)}`); + } + } + + // Handle already-encoded messages (Uint8Array from deserialization) + if (typeUrl && value instanceof Uint8Array) { + return { typeUrl, value } as MessageData; + } + + // Handle typed ExecuteContractMessage + const executeContractMessage = message as ExecuteContractMessage; this._utils.validateExecuteContractMessage(executeContractMessage, this.transactionType); return { typeUrl: constants.executeContractMsgTypeUrl, diff --git a/modules/abstract-cosmos/src/lib/constants.ts b/modules/abstract-cosmos/src/lib/constants.ts index b8c7b233aa..35e5461511 100644 --- a/modules/abstract-cosmos/src/lib/constants.ts +++ b/modules/abstract-cosmos/src/lib/constants.ts @@ -5,6 +5,7 @@ export const undelegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgUndelegate'; export const redelegateTypeUrl = '/cosmos.staking.v1beta1.MsgBeginRedelegate'; export const withdrawDelegatorRewardMsgTypeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; export const executeContractMsgTypeUrl = '/cosmwasm.wasm.v1.MsgExecuteContract'; +export const groupProposalMsgTypeUrl = '/cosmos.group.v1.MsgSubmitProposal'; export const UNAVAILABLE_TEXT = 'UNAVAILABLE'; export const ROOT_PATH = 'm/0'; export const sendMsgType = '/types.MsgSend'; // thorchain uses this custom message type diff --git a/modules/abstract-cosmos/src/lib/iface.ts b/modules/abstract-cosmos/src/lib/iface.ts index d0154b1bde..c21d1456b7 100644 --- a/modules/abstract-cosmos/src/lib/iface.ts +++ b/modules/abstract-cosmos/src/lib/iface.ts @@ -78,7 +78,7 @@ export type CosmosTransactionMessage = export interface MessageData { typeUrl: string; - value: CosmosTransactionMessage; + value: CosmosTransactionMessage | Uint8Array; // Uint8Array for pre-encoded messages } export interface FeeData { diff --git a/modules/abstract-cosmos/src/lib/transaction.ts b/modules/abstract-cosmos/src/lib/transaction.ts index 1e812dde13..4c99660434 100644 --- a/modules/abstract-cosmos/src/lib/transaction.ts +++ b/modules/abstract-cosmos/src/lib/transaction.ts @@ -228,6 +228,13 @@ export class CosmosTransaction extends BaseTransaction { explanationResult.type = TransactionType.ContractCall; outputAmount = BigInt(0); outputs = json.sendMessages.map((message) => { + //TODO: Handle pre-encoded contract call messages. + if (message.value instanceof Uint8Array) { + return { + address: UNAVAILABLE_TEXT, + amount: UNAVAILABLE_TEXT, + }; + } const executeContractMessage = message.value as ExecuteContractMessage; outputAmount = outputAmount + BigInt(executeContractMessage.funds?.[0]?.amount ?? '0'); return { @@ -324,6 +331,20 @@ export class CosmosTransaction extends BaseTransaction { break; case TransactionType.ContractCall: this.cosmosLikeTransaction.sendMessages.forEach((message) => { + if (message.value instanceof Uint8Array) { + //TODO: Handle pre-encoded contract call messages. + inputs.push({ + address: UNAVAILABLE_TEXT, + value: UNAVAILABLE_TEXT, + coin: this._coinConfig.name, + }); + outputs.push({ + address: UNAVAILABLE_TEXT, + value: UNAVAILABLE_TEXT, + coin: this._coinConfig.name, + }); + return; + } const executeContractMessage = message.value as ExecuteContractMessage; inputs.push({ address: executeContractMessage.sender, diff --git a/modules/abstract-cosmos/src/lib/transactionBuilder.ts b/modules/abstract-cosmos/src/lib/transactionBuilder.ts index 109a49231e..bb57666ea0 100644 --- a/modules/abstract-cosmos/src/lib/transactionBuilder.ts +++ b/modules/abstract-cosmos/src/lib/transactionBuilder.ts @@ -75,10 +75,10 @@ export abstract class CosmosTransactionBuilder extends Ba * - For @see TransactionType.Send required type is @see SendMessage * - For @see TransactionType.StakingWithdraw required type is @see WithdrawDelegatorRewardsMessage * - For @see TransactionType.ContractCall required type is @see ExecuteContractMessage - * @param {CosmosTransactionMessage[]} messages + * @param {CosmosTransactionMessage[] | MessageData[]} messages * @returns {TransactionBuilder} This transaction builder */ - abstract messages(messages: CosmosTransactionMessage[]): this; + abstract messages(messages: (CosmosTransactionMessage | MessageData)[]): this; publicKey(publicKey: string | undefined): this { this._publicKey = publicKey; @@ -166,11 +166,15 @@ export abstract class CosmosTransactionBuilder extends Ba this._transaction = tx; const txData = tx.toJson(); this.gasBudget(txData.gasBudget); - this.messages( + const messagesToSet: (MessageData | CosmosTransactionMessage)[] = txData.sendMessages.map((message) => { - return message.value; - }) - ); + if (message.value instanceof Uint8Array) { + return message; // Keep as MessageData for pre-encoded messages + } else { + return message.value; // Extract the actual message for typed messages + } + }); + this.messages(messagesToSet); this.sequence(txData.sequence); this.publicKey(txData.publicKey); this.accountNumber(txData.accountNumber); diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index 7c5c5f1818..3c3f8605a5 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -39,6 +39,7 @@ import { import { CosmosKeyPair as KeyPair } from './keyPair'; const { MsgSend } = require('../../resources/MsgCompiled').types; +const { MsgSubmitProposal } = require('../../resources/ProposalCompiled').cosmos.group.v1; export class CosmosUtils implements BaseUtils { protected registry; @@ -47,6 +48,7 @@ export class CosmosUtils implements BaseUtils { this.registry = new Registry([...defaultRegistryTypes]); this.registry.register(constants.executeContractMsgTypeUrl, MsgExecuteContract); this.registry.register('/types.MsgSend', MsgSend); + this.registry.register(constants.groupProposalMsgTypeUrl, MsgSubmitProposal); } /** @inheritdoc */ @@ -326,16 +328,27 @@ export class CosmosUtils implements BaseUtils { */ getExecuteContractMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { return decodedTx.body.messages.map((message) => { - const value = this.registry.decode(message); - return { - value: { - sender: value.sender, - contract: value.contract, - msg: value.msg, - funds: value.funds, - }, - typeUrl: message.typeUrl, - }; + if (message.typeUrl !== constants.groupProposalMsgTypeUrl) { + try { + const value = this.registry.decode(message); + return { + value: { + sender: value.sender, + contract: value.contract, + msg: value.msg, + funds: value.funds, + }, + typeUrl: message.typeUrl, + }; + } catch (error) { + throw new ParseTransactionError(`Error decoding execute contract message: ${error.message}`); + } + } else { + return { + value: message.value, + typeUrl: message.typeUrl, + } as MessageData; + } }); } @@ -367,6 +380,8 @@ export class CosmosUtils implements BaseUtils { return TransactionType.StakingWithdraw; case constants.executeContractMsgTypeUrl: return TransactionType.ContractCall; + case constants.groupProposalMsgTypeUrl: + return TransactionType.ContractCall; case constants.redelegateTypeUrl: return TransactionType.StakingRedelegate; default: @@ -390,7 +405,33 @@ export class CosmosUtils implements BaseUtils { * @returns {Any[]} processed send messages */ getSendMessagesForEncodingTx(cosmosLikeTransaction: CosmosLikeTransaction): Any[] { - return cosmosLikeTransaction.sendMessages as unknown as Any[]; + return cosmosLikeTransaction.sendMessages.map((msg) => { + if (msg.value instanceof Uint8Array) { + if (msg.typeUrl === constants.groupProposalMsgTypeUrl) { + // For group proposal messages, the pre-encoded bytes contain the full MsgSubmitProposal + try { + const decoded = this.registry.decode({ typeUrl: msg.typeUrl, value: msg.value }); + return { + typeUrl: msg.typeUrl, + value: decoded, + } as Any; + } catch (error) { + // If decoding fails, fall back to returning the bytes directly + return { + typeUrl: msg.typeUrl, + value: msg.value, + } as Any; + } + } else { + return { + typeUrl: msg.typeUrl, + value: msg.value, + } as Any; + } + } else { + return msg as unknown as Any; + } + }); } /** @@ -637,6 +678,10 @@ export class CosmosUtils implements BaseUtils { break; } case TransactionType.ContractCall: { + if (messageData.value instanceof Uint8Array) { + //TODO: Add validation for pre-encoded contract call messages + break; + } const value = messageData.value as ExecuteContractMessage; this.validateExecuteContractMessage(value, TransactionType.ContractCall); break; diff --git a/modules/sdk-coin-hash/test/resources/hash.ts b/modules/sdk-coin-hash/test/resources/hash.ts index f7cc21cdb4..71736eb79a 100644 --- a/modules/sdk-coin-hash/test/resources/hash.ts +++ b/modules/sdk-coin-hash/test/resources/hash.ts @@ -1,5 +1,24 @@ // Get the test data by running the scripts for the particular coin from coin-sandbox repo. +export const TEST_CONTRACT_CALL = { + //pre generated message using cosmos sdk and figure market apis + preEncodedMessageValue: + 'Ci0vcHJvdmVuYW5jZS5leGNoYW5nZS52MS5Nc2dDb21taXRGdW5kc1JlcXVlc3QSjQEKPXRwMXRhemVmd2syZTM3MmZ5MmpxMDh3Nmx6dGc5eXJydmM0OTByMmdwNHZ0OGQwZmNobHJmcXF5YWhnMHUQARoUCgl1eWxkcy5mY2MSBzEwMDAwMDAqNGV4Y2hhbmdlLWNvbW1pdDoyMWVhNjM0MC05OWFmLTRjOGMtOGZhMC03NWZlMGQxYmJkNDQ=', + pubKey: 'A58Dr1SAmP95RFbQXyrcw4nPwEq8bhbZJmmmJV4zFFsh', + privateKey: 'sW1/vJr2qhN8MtrM8oK6xGKDTBo1VxMdLEP4yJzcEz4=', + feeGranter: 'tp12vdnr7ddckx0m8u62qusrzq5r66cej5rd49zwf', + proposer: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', + chainId: 'pio-testnet-1', + accountNumber: 239218, + sequence: 16, + fee: '40000000000', + gasLimit: 250000, + messageTypeUrl: '/cosmos.group.v1.MsgSubmitProposal', + //pre generated message using cosmos sdk + expectedSignBytesHex: + '0ae9020ae6020a222f636f736d6f732e67726f75702e76312e4d73675375626d697450726f706f73616c12bf020a3d74703174617a6566776b32653337326679326a71303877366c7a7467397972727663343930723267703476743864306663686c726671717961686730751229747031326e796e3833796e6577746d706b773332777136646738337778386e71706174363567636c641a0f65786368616e67652d636f6d6d697422bf010a2d2f70726f76656e616e63652e65786368616e67652e76312e4d7367436f6d6d697446756e647352657175657374128d010a3d74703174617a6566776b32653337326679326a71303877366c7a7467397972727663343930723267703476743864306663686c7266717179616867307510011a140a0975796c64732e6663631207313030303030302a3465786368616e67652d636f6d6d69743a32316561363334302d393961662d346338632d386661302d37356665306431626264343428011299010a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21039f03af548098ff794456d05f2adcc389cfc04abc6e16d92669a6255e33145b2112040a020801181012450a140a056e68617368120b34303030303030303030301090a10f22297470313276646e72376464636b78306d38753632717573727a713572363663656a35726434397a77661a0d70696f2d746573746e65742d3120f2cc0e', +}; + export const TEST_ACCOUNT = { pubAddress: 'pb1496r8u4a48k6khknrhzd6c8cm3c64ewxhlyxpc', testnetPubAddress: 'tp1496r8u4a48k6khknrhzd6c8cm3c64ewxy5p2rj', diff --git a/modules/sdk-coin-hash/test/unit/transactionBuilder/contractCallBuilder.ts b/modules/sdk-coin-hash/test/unit/transactionBuilder/contractCallBuilder.ts new file mode 100644 index 0000000000..9a3dd11f8d --- /dev/null +++ b/modules/sdk-coin-hash/test/unit/transactionBuilder/contractCallBuilder.ts @@ -0,0 +1,100 @@ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TransactionType } from '@bitgo/sdk-core'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { fromBase64, toHex } from '@cosmjs/encoding'; +import should from 'should'; +import { Hash, Thash } from '../../../src'; +import { TEST_CONTRACT_CALL, testnetAddress } from '../../resources/hash'; +import { wrapInGroupProposal } from '../../utils/groupProposalHelper'; + +describe('Hash ContractCall Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + + // Helper function to create a complete transaction builder with standard settings and message + const contractCallBuilder = () => { + const txBuilder = factory.getContractCallBuilder(); + txBuilder.sequence(TEST_CONTRACT_CALL.sequence); + txBuilder.accountNumber(TEST_CONTRACT_CALL.accountNumber); + txBuilder.chainId(TEST_CONTRACT_CALL.chainId); + txBuilder.gasBudget({ + amount: [{ denom: 'nhash', amount: TEST_CONTRACT_CALL.fee }], + gasLimit: TEST_CONTRACT_CALL.gasLimit, + }); + txBuilder.feeGranter(TEST_CONTRACT_CALL.feeGranter); + txBuilder.publicKey(toHex(fromBase64(TEST_CONTRACT_CALL.pubKey))); + + // Wrap the inner message in a group proposal + const wrappedMessage = wrapInGroupProposal( + TEST_CONTRACT_CALL.preEncodedMessageValue, + TEST_CONTRACT_CALL.proposer, + testnetAddress.groupPolicyAddress + ); + + txBuilder.messages([ + { + typeUrl: '/cosmos.group.v1.MsgSubmitProposal', + value: wrappedMessage.value, + }, + ]); + return txBuilder; + }; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('hash', Hash.createInstance); + bitgo.safeRegister('thash', Thash.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('thash'); + factory = basecoin.getBuilder(); + }); + + describe('Contract Call Builder Tests', () => { + it('should build transaction with expected signable payload', async function () { + const txBuilder = contractCallBuilder(); + const tx = await txBuilder.build(); + should.equal(toHex(tx.signablePayload), TEST_CONTRACT_CALL.expectedSignBytesHex); + should.equal(tx.type, TransactionType.ContractCall); + }); + + it('should build, sign, and serialize contract call transactions', async function () { + // Test unsigned transaction building + const unsignedBuilder = contractCallBuilder(); + const unsignedTx = await unsignedBuilder.build(); + should.equal(unsignedTx.type, TransactionType.ContractCall); + should.equal(unsignedTx.signature.length, 0); + should.exist(unsignedTx.toBroadcastFormat()); + + // Test signing functionality + const signedBuilder = contractCallBuilder(); + signedBuilder.sign({ key: toHex(fromBase64(TEST_CONTRACT_CALL.privateKey)) }); + const signedTx = await signedBuilder.build(); + should.equal(signedTx.type, TransactionType.ContractCall); + should.equal(signedTx.signature.length, 1); + should.exist(signedTx.toBroadcastFormat()); + }); + + it('should handle round-trip serialization for signed and unsigned transactions', async function () { + // Test unsigned serialization + const unsignedBuilder = contractCallBuilder(); + const unsignedTx = await unsignedBuilder.build(); + const unsignedRaw = unsignedTx.toBroadcastFormat(); + const rebuiltUnsigned = factory.from(unsignedRaw); + const rebuiltUnsignedTx = await rebuiltUnsigned.build(); + should.equal(rebuiltUnsignedTx.toBroadcastFormat(), unsignedRaw); + should.equal(rebuiltUnsignedTx.type, TransactionType.ContractCall); + + // Test signed serialization + const signedBuilder = contractCallBuilder(); + signedBuilder.sign({ key: toHex(fromBase64(TEST_CONTRACT_CALL.privateKey)) }); + const signedTx = await signedBuilder.build(); + const signedRaw = signedTx.toBroadcastFormat(); + const rebuiltSigned = factory.from(signedRaw); + const rebuiltSignedTx = await rebuiltSigned.build(); + should.equal(rebuiltSignedTx.toBroadcastFormat(), signedRaw); + should.equal(rebuiltSignedTx.signature.length, 1); + should.equal(rebuiltSignedTx.signature[0], signedTx.signature[0]); + }); + }); +}); diff --git a/modules/sdk-coin-hash/test/utils/groupProposalHelper.ts b/modules/sdk-coin-hash/test/utils/groupProposalHelper.ts new file mode 100644 index 0000000000..16a0793ad0 --- /dev/null +++ b/modules/sdk-coin-hash/test/utils/groupProposalHelper.ts @@ -0,0 +1,33 @@ +import { fromBase64, toBase64 } from '@cosmjs/encoding'; +import { Any } from 'cosmjs-types/google/protobuf/any.js'; +import { MsgSubmitProposal, Exec } from 'cosmjs-types/cosmos/group/v1/tx.js'; + +/** + * Wraps an inner message in a MsgSubmitProposal for group transactions + * @param innerMsgBase64 - The base64 encoded inner message + * @param proposer - The proposer address + * @param groupPolicyAddress - The group policy address + * @returns The wrapped message with base64 value for better debugging + */ +export function wrapInGroupProposal( + innerMsgBase64: string, + proposer: string, + groupPolicyAddress: string +): { + typeUrl: string; + value: string; +} { + const innerMsg = Any.decode(fromBase64(innerMsgBase64)); + const proposal = MsgSubmitProposal.fromPartial({ + groupPolicyAddress, + proposers: [proposer], + metadata: 'exchange-commit', + messages: [innerMsg], + exec: Exec.EXEC_TRY, + }); + + return { + typeUrl: '/cosmos.group.v1.MsgSubmitProposal', + value: toBase64(MsgSubmitProposal.encode(proposal).finish()), + }; +} From ebd491cd8dd8252d258e6195f2ab2b8fcb1b8336 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:23:41 +0000 Subject: [PATCH 2/2] Initial plan