Skip to content

Commit

Permalink
Merge pull request #3691 from BitGo/cosmos-contract-txn
Browse files Browse the repository at this point in the history
feat(cosmos): Add support for contract call transaction
  • Loading branch information
dpkjnr committed Jun 27, 2023
2 parents 7d43da1 + 9b7dd54 commit 0bd8547
Show file tree
Hide file tree
Showing 41 changed files with 576 additions and 116 deletions.
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) {
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,
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']);
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']);
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');
}

/**
* 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})$/;
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

0 comments on commit 0bd8547

Please sign in to comment.