Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cosmos): Add support for contract call transaction #3691

Merged
merged 1 commit into from
Jun 27, 2023
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
4 changes: 2 additions & 2 deletions modules/abstract-cosmos/src/cosmosCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export class CosmosCoin extends BaseCoin {
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
throw new Error('Tx outputs does not match with expected txParams recipients');
}
// WithdrawDelegatorRewards transaction doesn't have amount
if (transaction.type !== TransactionType.StakingWithdraw) {
// WithdrawDelegatorRewards and ContractCall transaction don't have amount
if (transaction.type !== TransactionType.StakingWithdraw && transaction.type !== TransactionType.ContractCall) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contract interaction support the amount, anyway that will not require in our case but let's keep in mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it is under funds and didn't want to update current amount validation for it

for (const recipients of txParams.recipients) {
totalAmount = totalAmount.plus(recipients.amount);
}
Expand Down
32 changes: 32 additions & 0 deletions modules/abstract-cosmos/src/lib/ContractCallBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';

import * as constants from './constants';
import { ExecuteContractMessage } from './iface';
import { CosmosTransactionBuilder } from './transactionBuilder';
import { CosmosUtils } from './utils';

export class ContractCallBuilder extends CosmosTransactionBuilder {
protected _utils: CosmosUtils;

constructor(_coinConfig: Readonly<CoinConfig>, utils: CosmosUtils) {
super(_coinConfig, utils);
this._utils = utils;
}

protected get transactionType(): TransactionType {
return TransactionType.ContractCall;
}

/** @inheritdoc */
messages(messages: ExecuteContractMessage[]): this {
this._messages = messages.map((executeContractMessage) => {
this._utils.validateExecuteContractMessage(executeContractMessage);
return {
typeUrl: constants.executeContractMsgTypeUrl,
value: executeContractMessage,
};
});
return this;
}
}
1 change: 1 addition & 0 deletions modules/abstract-cosmos/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const sendMsgTypeUrl = '/cosmos.bank.v1beta1.MsgSend';
export const delegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgDelegate';
export const undelegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgUndelegate';
export const withdrawDelegatorRewardMsgTypeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward';
export const executeContractMsgTypeUrl = '/cosmwasm.wasm.v1.MsgExecuteContract';
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
23 changes: 18 additions & 5 deletions modules/abstract-cosmos/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ export interface TransactionExplanation extends BaseTransactionExplanation {
type: TransactionType;
}

export interface MessageData {
typeUrl: string;
value: SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage;
}

export interface SendMessage {
fromAddress: string;
toAddress: string;
Expand All @@ -27,6 +22,24 @@ export interface WithdrawDelegatorRewardsMessage {
validatorAddress: string;
}

export interface ExecuteContractMessage {
sender: string;
contract: string;
msg: Uint8Array;
funds?: Coin[];
}

export type CosmosTransactionMessage =
| SendMessage
| DelegateOrUndelegeteMessage
| WithdrawDelegatorRewardsMessage
| ExecuteContractMessage;

export interface MessageData {
typeUrl: string;
value: CosmosTransactionMessage;
}

export interface FeeData {
amount: Coin[];
gasLimit: number;
Expand Down
1 change: 1 addition & 0 deletions modules/abstract-cosmos/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './iface';
export { StakingActivateBuilder } from './StakingActivateBuilder';
export { StakingDeactivateBuilder } from './StakingDeactivateBuilder';
export { StakingWithdrawRewardsBuilder } from './StakingWithdrawRewardsBuilder';
export { ContractCallBuilder } from './ContractCallBuilder';
export { CosmosKeyPair } from './keyPair';
export { CosmosTransaction } from './transaction';
export { CosmosTransactionBuilder } from './transactionBuilder';
Expand Down
26 changes: 26 additions & 0 deletions modules/abstract-cosmos/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { UNAVAILABLE_TEXT } from './constants';
import {
CosmosLikeTransaction,
DelegateOrUndelegeteMessage,
ExecuteContractMessage,
SendMessage,
TransactionExplanation,
TxData,
Expand Down Expand Up @@ -218,6 +219,18 @@ export class CosmosTransaction extends BaseTransaction {
amount: UNAVAILABLE_TEXT,
},
];
outputAmount = UNAVAILABLE_TEXT;
break;
case TransactionType.ContractCall:
explanationResult.type = TransactionType.ContractCall;
message = json.sendMessages[0].value as ExecuteContractMessage;
outputs = [
{
address: message.contract,
amount: UNAVAILABLE_TEXT,
},
];
outputAmount = UNAVAILABLE_TEXT;
break;
default:
throw new InvalidTransactionError('Transaction type not supported');
Expand Down Expand Up @@ -287,6 +300,19 @@ export class CosmosTransaction extends BaseTransaction {
coin: this._coinConfig.name,
});
break;
case TransactionType.ContractCall:
const executeContractMessage = this.cosmosLikeTransaction.sendMessages[0].value as ExecuteContractMessage;
inputs.push({
address: executeContractMessage.sender,
value: UNAVAILABLE_TEXT,
coin: this._coinConfig.name,
});
outputs.push({
address: executeContractMessage.contract,
value: UNAVAILABLE_TEXT,
coin: this._coinConfig.name,
});
break;
default:
throw new InvalidTransactionError('Transaction type not supported');
}
Expand Down
16 changes: 5 additions & 11 deletions modules/abstract-cosmos/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import {
BaseAddress,
BaseKey,
PublicKey as BasePublicKey,
BaseTransactionBuilder,
BuildTransactionError,
InvalidTransactionError,
PublicKey as BasePublicKey,
SigningError,
TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Secp256k1, sha256 } from '@cosmjs/crypto';
import { makeSignBytes } from '@cosmjs/proto-signing';
import BigNumber from 'bignumber.js';

import {
DelegateOrUndelegeteMessage,
FeeData,
MessageData,
SendMessage,
WithdrawDelegatorRewardsMessage,
} from './iface';
import { CosmosTransactionMessage, FeeData, MessageData } from './iface';
import { CosmosKeyPair as KeyPair } from './keyPair';
import { CosmosTransaction } from './transaction';
import { CosmosUtils } from './utils';
Expand Down Expand Up @@ -81,10 +74,11 @@ export abstract class CosmosTransactionBuilder extends BaseTransactionBuilder {
* - For @see TransactionType.StakingDeactivate required type is @see DelegateOrUndelegeteMessage
* - For @see TransactionType.Send required type is @see SendMessage
* - For @see TransactionType.StakingWithdraw required type is @see WithdrawDelegatorRewardsMessage
* @param {(SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage)[]} messages
* - For @see TransactionType.ContractCall required type is @see ExecuteContractMessage
* @param {CosmosTransactionMessage[]} messages
* @returns {TransactionBuilder} This transaction builder
*/
abstract messages(messages: (SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage)[]): this;
abstract messages(messages: CosmosTransactionMessage[]): this;

publicKey(publicKey: string | undefined): this {
this._publicKey = publicKey;
Expand Down
74 changes: 69 additions & 5 deletions modules/abstract-cosmos/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import { Coin, defaultRegistryTypes } from '@cosmjs/stargate';
import BigNumber from 'bignumber.js';
import { SignDoc, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
import { Any } from 'cosmjs-types/google/protobuf/any';
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx';

import * as crypto from 'crypto';
import * as constants from './constants';
import {
CosmosLikeTransaction,
DelegateOrUndelegeteMessage,
ExecuteContractMessage,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use CosmosTransactionMessage here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't, schema is different

FeeData,
MessageData,
SendMessage,
Expand All @@ -35,7 +37,12 @@ import {
import { CosmosKeyPair as KeyPair } from './keyPair';

export class CosmosUtils implements BaseUtils {
private registry = new Registry([...defaultRegistryTypes]);
private registry;

constructor() {
this.registry = new Registry([...defaultRegistryTypes]);
this.registry.register(constants.executeContractMsgTypeUrl, MsgExecuteContract);
}

/** @inheritdoc */
isValidBlockId(hash: string): boolean {
Expand Down Expand Up @@ -260,6 +267,26 @@ export class CosmosUtils implements BaseUtils {
});
}

/**
* Returns the array of MessageData[] from the decoded transaction
* @param {DecodedTxRaw} decodedTx
* @returns {MessageData[]} Execute contract transaction message data
*/
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,
};
});
}

/**
* Determines bitgo transaction type based on cosmos proto type url
* @param {string} typeUrl
Expand All @@ -275,6 +302,8 @@ export class CosmosUtils implements BaseUtils {
return TransactionType.StakingDeactivate;
case constants.withdrawDelegatorRewardMsgTypeUrl:
return TransactionType.StakingWithdraw;
case constants.executeContractMsgTypeUrl:
return TransactionType.ContractCall;
default:
return undefined;
}
Expand Down Expand Up @@ -473,19 +502,23 @@ export class CosmosUtils implements BaseUtils {
switch (type) {
case TransactionType.Send: {
const value = messageData.value as SendMessage;
this.isObjPropertyNull(value, ['toAddress', 'fromAddress']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we removing this null check ??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is being done in this.validateSendMessage, I wanted to keep validation code common

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check this function's path - enrichTransactionDetailsFromRawTransaction()
I think we wont be making this null check here. As this is not calling this.validateSendMessage.

this.validateSendMessage(value);
break;
}
case TransactionType.StakingActivate:
case TransactionType.StakingDeactivate: {
const value = messageData.value as DelegateOrUndelegeteMessage;
this.isObjPropertyNull(value, ['validatorAddress', 'delegatorAddress']);
this.validateAmount(value.amount);
this.validateDelegateOrUndelegateMessage(value);
break;
}
case TransactionType.StakingWithdraw: {
const value = messageData.value as WithdrawDelegatorRewardsMessage;
this.isObjPropertyNull(value, ['validatorAddress', 'delegatorAddress']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

this.validateWithdrawRewardsMessage(value);
break;
}
case TransactionType.ContractCall: {
const value = messageData.value as ExecuteContractMessage;
this.validateExecuteContractMessage(value);
break;
}
default:
Expand Down Expand Up @@ -591,6 +624,8 @@ export class CosmosUtils implements BaseUtils {
sendMessageData = this.getDelegateOrUndelegateMessageDataFromDecodedTx(decodedTx);
} else if (type === TransactionType.StakingWithdraw) {
sendMessageData = this.getWithdrawRewardsMessageDataFromDecodedTx(decodedTx);
} else if (type === TransactionType.ContractCall) {
sendMessageData = this.getExecuteContractMessageDataFromDecodedTx(decodedTx);
} else {
throw new Error('Transaction type not supported: ' + typeUrl);
}
Expand Down Expand Up @@ -671,6 +706,35 @@ export class CosmosUtils implements BaseUtils {
isValidAddress(address: string): boolean {
throw new NotImplementedError('isValidAddress not implemented');
}

/**
* Validates if the address matches with regex @see contractAddressRegex
* @param {string} address
* @returns {boolean} - the validation result
*/
isValidContractAddress(address: string): boolean {
throw new NotImplementedError('isValidContractAddress not implemented');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's implement using the length comparison and add ToDo to implement with Regex. Else it will be blocker in the platform side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is there in extended classes

}

/**
* Validates a execute contract message
* @param {ExecuteContractMessage} message - The execute contract message to validate
* @throws {InvalidTransactionError} Throws an error if the message is invalid
*/
validateExecuteContractMessage(message: ExecuteContractMessage) {
if (!message.contract || !this.isValidContractAddress(message.contract)) {
throw new InvalidTransactionError(`Invalid ExecuteContractMessage contract address: ` + message.contract);
}
if (!message.sender || !this.isValidAddress(message.sender)) {
throw new InvalidTransactionError(`Invalid ExecuteContractMessage sender address: ` + message.sender);
}
if (!message.msg) {
throw new InvalidTransactionError(`Invalid ExecuteContractMessage msg: ` + message.msg);
}
if (message.funds) {
this.validateAmountData(message.funds);
}
}
}

const utils = new CosmosUtils();
Expand Down
5 changes: 3 additions & 2 deletions modules/sdk-coin-atom/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ export const delegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgDelegate';
export const undelegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgUndelegate';
export const withdrawDelegatorRewardMsgTypeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward';
export const validDenoms = ['natom', 'uatom', 'matom', 'atom'];
export const accountAddressRegex = /^(cosmos)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const validatorAddressRegex = /^(cosmosvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const accountAddressRegex = /^(cosmos)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
export const validatorAddressRegex = /^(cosmosvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What changed here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length, for account address it would always be 38 character long(excluding prefix)

export const contractAddressRegex = /^(cosmos)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
export const GAS_AMOUNT = '100000';
export const GAS_LIMIT = 200000;
5 changes: 3 additions & 2 deletions modules/sdk-coin-bld/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const validDenoms = ['nbld', 'ubld', 'mbld', 'bld'];
export const accountAddressRegex = /^(agoric)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const validatorAddressRegex = /^(agoricvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const accountAddressRegex = /^(agoric)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
export const validatorAddressRegex = /^(agoricvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
export const contractAddressRegex = /^(agoric)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
7 changes: 7 additions & 0 deletions modules/sdk-coin-bld/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StakingActivateBuilder,
StakingDeactivateBuilder,
StakingWithdrawRewardsBuilder,
ContractCallBuilder,
} from '@bitgo/abstract-cosmos';
import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
Expand All @@ -29,6 +30,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.getStakingDeactivateBuilder(tx);
case TransactionType.StakingWithdraw:
return this.getStakingWithdrawRewardsBuilder(tx);
case TransactionType.ContractCall:
return this.getContractCallBuilder(tx);
default:
throw new InvalidTransactionError('Invalid transaction');
}
Expand Down Expand Up @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, bldUtils));
}

getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder {
return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, bldUtils));
}

/** @inheritdoc */
getWalletInitializationBuilder(): void {
throw new Error('Method not implemented.');
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-coin-bld/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export class BldUtils extends CosmosUtils {
return constants.validatorAddressRegex.test(address);
}

/** @inheritdoc */
isValidContractAddress(address: string): boolean {
return constants.contractAddressRegex.test(address);
}

/** @inheritdoc */
validateAmount(amount: Coin): void {
const amountBig = BigNumber(amount.amount);
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-bld/test/unit/bld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('BLD', function () {
amount: 'UNAVAILABLE',
},
],
outputAmount: undefined,
outputAmount: 'UNAVAILABLE',
changeOutputs: [],
changeAmount: '0',
fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount },
Expand Down
5 changes: 3 additions & 2 deletions modules/sdk-coin-hash/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const validDenoms = ['nhash', 'uhash', 'mhash', 'hash'];
export const accountAddressRegex = /^(tp|pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const validatorAddressRegex = /^(tpvaloper|pbvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
export const accountAddressRegex = /^(tp|pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
export const validatorAddressRegex = /^(tpvaloper|pbvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/;
export const contractAddressRegex = /^(tp|pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/;
Loading