diff --git a/modules/abstract-cosmos/src/cosmosCoin.ts b/modules/abstract-cosmos/src/cosmosCoin.ts index dcad570b38..396e9c7235 100644 --- a/modules/abstract-cosmos/src/cosmosCoin.ts +++ b/modules/abstract-cosmos/src/cosmosCoin.ts @@ -129,7 +129,7 @@ export class CosmosCoin extends BaseCoin { throw new Error('Tx outputs does not match with expected txParams recipients'); } // WithdrawDelegatorRewards transaction doesn't have amount - if (transaction.type !== TransactionType.StakingWithdraw) { + if (transaction.type !== TransactionType.StakingWithdraw && transaction.type !== TransactionType.ContractCall) { for (const recipients of txParams.recipients) { totalAmount = totalAmount.plus(recipients.amount); } diff --git a/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts new file mode 100644 index 0000000000..8c4de25256 --- /dev/null +++ b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts @@ -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, 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; + } +} diff --git a/modules/abstract-cosmos/src/lib/constants.ts b/modules/abstract-cosmos/src/lib/constants.ts index f640f3d29e..0c3227e407 100644 --- a/modules/abstract-cosmos/src/lib/constants.ts +++ b/modules/abstract-cosmos/src/lib/constants.ts @@ -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'; diff --git a/modules/abstract-cosmos/src/lib/iface.ts b/modules/abstract-cosmos/src/lib/iface.ts index 010ecc22b5..788a24c63d 100644 --- a/modules/abstract-cosmos/src/lib/iface.ts +++ b/modules/abstract-cosmos/src/lib/iface.ts @@ -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; @@ -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; diff --git a/modules/abstract-cosmos/src/lib/index.ts b/modules/abstract-cosmos/src/lib/index.ts index bb53ee33b4..7557a85624 100644 --- a/modules/abstract-cosmos/src/lib/index.ts +++ b/modules/abstract-cosmos/src/lib/index.ts @@ -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'; diff --git a/modules/abstract-cosmos/src/lib/transaction.ts b/modules/abstract-cosmos/src/lib/transaction.ts index cd5376cfd1..7d1a420b7c 100644 --- a/modules/abstract-cosmos/src/lib/transaction.ts +++ b/modules/abstract-cosmos/src/lib/transaction.ts @@ -17,6 +17,7 @@ import { UNAVAILABLE_TEXT } from './constants'; import { CosmosLikeTransaction, DelegateOrUndelegeteMessage, + ExecuteContractMessage, SendMessage, TransactionExplanation, TxData, @@ -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'); @@ -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'); } diff --git a/modules/abstract-cosmos/src/lib/transactionBuilder.ts b/modules/abstract-cosmos/src/lib/transactionBuilder.ts index 0f725917ea..65509138a9 100644 --- a/modules/abstract-cosmos/src/lib/transactionBuilder.ts +++ b/modules/abstract-cosmos/src/lib/transactionBuilder.ts @@ -1,10 +1,10 @@ import { BaseAddress, BaseKey, - PublicKey as BasePublicKey, BaseTransactionBuilder, BuildTransactionError, InvalidTransactionError, + PublicKey as BasePublicKey, SigningError, TransactionType, } from '@bitgo/sdk-core'; @@ -12,14 +12,7 @@ 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'; @@ -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; diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index fe4b4c0be2..dbf064de90 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -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, @@ -35,7 +37,11 @@ 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('/cosmwasm.wasm.v1.MsgExecuteContract', MsgExecuteContract); + } /** @inheritdoc */ isValidBlockId(hash: string): boolean { @@ -260,6 +266,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 @@ -275,6 +301,8 @@ export class CosmosUtils implements BaseUtils { return TransactionType.StakingDeactivate; case constants.withdrawDelegatorRewardMsgTypeUrl: return TransactionType.StakingWithdraw; + case constants.executeContractMsgTypeUrl: + return TransactionType.ContractCall; default: return undefined; } @@ -473,19 +501,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: @@ -591,6 +623,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); } @@ -671,6 +705,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(); diff --git a/modules/sdk-coin-atom/src/lib/constants.ts b/modules/sdk-coin-atom/src/lib/constants.ts index 7513939ef1..f4042f628e 100644 --- a/modules/sdk-coin-atom/src/lib/constants.ts +++ b/modules/sdk-coin-atom/src/lib/constants.ts @@ -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; diff --git a/modules/sdk-coin-bld/src/lib/constants.ts b/modules/sdk-coin-bld/src/lib/constants.ts index f549794d52..3281fb8d71 100644 --- a/modules/sdk-coin-bld/src/lib/constants.ts +++ b/modules/sdk-coin-bld/src/lib/constants.ts @@ -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']+)$/; diff --git a/modules/sdk-coin-bld/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-bld/src/lib/transactionBuilderFactory.ts index 0a5e24c077..2fb4119ac8 100644 --- a/modules/sdk-coin-bld/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-bld/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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'); } @@ -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.'); diff --git a/modules/sdk-coin-bld/src/lib/utils.ts b/modules/sdk-coin-bld/src/lib/utils.ts index f91e988586..1955f85d59 100644 --- a/modules/sdk-coin-bld/src/lib/utils.ts +++ b/modules/sdk-coin-bld/src/lib/utils.ts @@ -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); diff --git a/modules/sdk-coin-bld/test/unit/bld.ts b/modules/sdk-coin-bld/test/unit/bld.ts index f39c600f1e..c85c8a8bfc 100644 --- a/modules/sdk-coin-bld/test/unit/bld.ts +++ b/modules/sdk-coin-bld/test/unit/bld.ts @@ -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 }, diff --git a/modules/sdk-coin-hash/src/lib/constants.ts b/modules/sdk-coin-hash/src/lib/constants.ts index 619dae9836..5792bc0318 100644 --- a/modules/sdk-coin-hash/src/lib/constants.ts +++ b/modules/sdk-coin-hash/src/lib/constants.ts @@ -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']+)$/; diff --git a/modules/sdk-coin-hash/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-hash/src/lib/transactionBuilderFactory.ts index 733153ed39..a867dafa1d 100644 --- a/modules/sdk-coin-hash/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-hash/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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'); } @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, hashUtils)); } + getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder { + return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, hashUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-hash/src/lib/utils.ts b/modules/sdk-coin-hash/src/lib/utils.ts index 6ca65fdad7..dd0da12676 100644 --- a/modules/sdk-coin-hash/src/lib/utils.ts +++ b/modules/sdk-coin-hash/src/lib/utils.ts @@ -16,6 +16,11 @@ export class HashUtils 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); diff --git a/modules/sdk-coin-hash/test/unit/hash.ts b/modules/sdk-coin-hash/test/unit/hash.ts index 9d6e11127b..9f8ee1ab00 100644 --- a/modules/sdk-coin-hash/test/unit/hash.ts +++ b/modules/sdk-coin-hash/test/unit/hash.ts @@ -260,7 +260,7 @@ describe('HASH', function () { amount: 'UNAVAILABLE', }, ], - outputAmount: undefined, + outputAmount: 'UNAVAILABLE', changeOutputs: [], changeAmount: '0', fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount }, diff --git a/modules/sdk-coin-injective/src/lib/constants.ts b/modules/sdk-coin-injective/src/lib/constants.ts index 7a13ced992..7982901c76 100644 --- a/modules/sdk-coin-injective/src/lib/constants.ts +++ b/modules/sdk-coin-injective/src/lib/constants.ts @@ -1,3 +1,4 @@ export const validDenoms = ['ninj', 'uinj', 'minj', 'inj']; export const accountAddressRegex = /^(inj)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; export const validatorAddressRegex = /^(injvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; +export const contractAddressRegex = /^(inj)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; diff --git a/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts index 9f5cd9b7a2..3f43275532 100644 --- a/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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'); } @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, injUtils)); } + getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder { + return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, injUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-injective/src/lib/utils.ts b/modules/sdk-coin-injective/src/lib/utils.ts index 02ef141318..b5c0d33ddf 100644 --- a/modules/sdk-coin-injective/src/lib/utils.ts +++ b/modules/sdk-coin-injective/src/lib/utils.ts @@ -16,6 +16,11 @@ export class InjectiveUtils 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); diff --git a/modules/sdk-coin-injective/test/unit/injective.ts b/modules/sdk-coin-injective/test/unit/injective.ts index 4a5b5b7487..4b973fc636 100644 --- a/modules/sdk-coin-injective/test/unit/injective.ts +++ b/modules/sdk-coin-injective/test/unit/injective.ts @@ -260,7 +260,7 @@ describe('INJ', function () { amount: 'UNAVAILABLE', }, ], - outputAmount: undefined, + outputAmount: 'UNAVAILABLE', changeOutputs: [], changeAmount: '0', fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount }, diff --git a/modules/sdk-coin-osmo/src/lib/constants.ts b/modules/sdk-coin-osmo/src/lib/constants.ts index 000c7c63f6..2c21f1e7ff 100644 --- a/modules/sdk-coin-osmo/src/lib/constants.ts +++ b/modules/sdk-coin-osmo/src/lib/constants.ts @@ -1,3 +1,4 @@ export const validDenoms = ['nosmo', 'uosmo', 'mosmo', 'osmo']; -export const accountAddressRegex = /^(osmo)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; -export const validatorAddressRegex = /^(osmovaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; +export const accountAddressRegex = /^(osmo)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const validatorAddressRegex = /^(osmovaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const contractAddressRegex = /^(osmo)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; diff --git a/modules/sdk-coin-osmo/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-osmo/src/lib/transactionBuilderFactory.ts index a495921844..649d9f8ca6 100644 --- a/modules/sdk-coin-osmo/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-osmo/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -29,8 +30,10 @@ 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'); + throw new InvalidTransactionError('Transaction type not supported'); } } catch (e) { throw new InvalidTransactionError('Invalid transaction: ' + e.message); @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, osmoUtils)); } + getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder { + return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, osmoUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-osmo/src/lib/utils.ts b/modules/sdk-coin-osmo/src/lib/utils.ts index cbd218b3c6..ea60f88b43 100644 --- a/modules/sdk-coin-osmo/src/lib/utils.ts +++ b/modules/sdk-coin-osmo/src/lib/utils.ts @@ -16,6 +16,11 @@ export class OsmoUtils 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); diff --git a/modules/sdk-coin-osmo/test/resources/osmo.ts b/modules/sdk-coin-osmo/test/resources/osmo.ts index 949172f43e..56e570a05a 100644 --- a/modules/sdk-coin-osmo/test/resources/osmo.ts +++ b/modules/sdk-coin-osmo/test/resources/osmo.ts @@ -18,8 +18,8 @@ export const TEST_SEND_TX = { privateKey: 'Z3oU9gNXXcifekIRlVe1fmoYIAbn0luis55Xw/Zbmko=', signedTxBase64: 'Co4BCosBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmsKK29zbW8xbWVtdHNtbHo5NWdmOTM4YWdmNXE2cWZoaGh0NHBwZThtZWpjdXMSK29zbW8xamo2NWR3MnF1bjRyZHg2d3V2dGM0dTh3YWRoaHp3bXU3enZsdzMaDwoFdW9zbW8SBjEwMDAwMBJkCk4KRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEC+YLTNgkMbmpyrpsQVcZ4ySyXp9hjApsl6vd8cqPV4ZQSBAoCCAESEgoMCgV1b3NtbxIDNTAwEMCaDBpAQSYBjVIIMa1ILAong0RZ7NFiLbJiXMmyTso6bP+qt+05jsRMX1rPVGPy5izbqP9DF7NNNkUHt+Th05szBCVIwA==', - sender: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', - recipient: 'osmo1jj65dw2qun4rdx6wuvtc4u8wadhhzwmu7zvlw3', + from: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', + to: 'osmo1jj65dw2qun4rdx6wuvtc4u8wadhhzwmu7zvlw3', chainId: 'osmo-test-5', accountNumber: 1346, sequence: 0, @@ -51,8 +51,8 @@ export const TEST_DELEGATE_TX = { privateKey: 'Z3oU9gNXXcifekIRlVe1fmoYIAbn0luis55Xw/Zbmko=', signedTxBase64: 'CpsBCpgBCiMvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dEZWxlZ2F0ZRJxCitvc21vMW1lbXRzbWx6OTVnZjkzOGFnZjVxNnFmaGhodDRwcGU4bWVqY3VzEjJvc21vdmFsb3BlcjFoaDBnNXhmMjNlNXpla2c0NWNtZXJjOTdoczRuMjAwNGR5MnQyNhoOCgV1b3NtbxIFMTAwMDASZgpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAvmC0zYJDG5qcq6bEFXGeMksl6fYYwKbJer3fHKj1eGUEgQKAggBGAESEgoMCgV1b3NtbxIDNTAwEMCaDBpAZJh1FygiLSB1W55WQkknTjy/V57mkUK/3ibM5ed4LX1llCXcoNaCbqlIRXgGbatmGjn03UeH3nZsN5zE74s65w==', - delegator: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', - validator: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', + from: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', + to: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', chainId: 'osmo-test-5', accountNumber: 1346, sequence: 1, @@ -87,8 +87,8 @@ export const TEST_UNDELEGATE_TX = { privateKey: 'Z3oU9gNXXcifekIRlVe1fmoYIAbn0luis55Xw/Zbmko=', signedTxBase64: 'Cp0BCpoBCiUvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dVbmRlbGVnYXRlEnEKK29zbW8xbWVtdHNtbHo5NWdmOTM4YWdmNXE2cWZoaGh0NHBwZThtZWpjdXMSMm9zbW92YWxvcGVyMWhoMGc1eGYyM2U1emVrZzQ1Y21lcmM5N2hzNG4yMDA0ZHkydDI2Gg4KBXVvc21vEgUxMDAwMBJmClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEC+YLTNgkMbmpyrpsQVcZ4ySyXp9hjApsl6vd8cqPV4ZQSBAoCCAEYAhISCgwKBXVvc21vEgM1MDAQwJoMGkDxePYvJZHYu2h9vT01JwEt4YGVNdgJ/zFb8vC9KuCwM1+GCtYDe8xaWF6Tq56d0YxaTIBY5w7Ya2OEqJkqx1zJ', - delegator: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', - validator: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', + from: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', + to: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', chainId: 'osmo-test-5', accountNumber: 1346, sequence: 2, @@ -123,8 +123,8 @@ export const TEST_WITHDRAW_REWARDS_TX = { privateKey: 'Z3oU9gNXXcifekIRlVe1fmoYIAbn0luis55Xw/Zbmko=', signedTxBase64: 'Cp8BCpwBCjcvY29zbW9zLmRpc3RyaWJ1dGlvbi52MWJldGExLk1zZ1dpdGhkcmF3RGVsZWdhdG9yUmV3YXJkEmEKK29zbW8xbWVtdHNtbHo5NWdmOTM4YWdmNXE2cWZoaGh0NHBwZThtZWpjdXMSMm9zbW92YWxvcGVyMWhoMGc1eGYyM2U1emVrZzQ1Y21lcmM5N2hzNG4yMDA0ZHkydDI2EmYKUApGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQL5gtM2CQxuanKumxBVxnjJLJen2GMCmyXq93xyo9XhlBIECgIIARgDEhIKDAoFdW9zbW8SAzUwMBDAmgwaQMXVzLnOx/zwDe2FNgDLlaZF+XN1SlznSXCu7iBnBi7KIgp6B1bIJ8RygtJkL59LTutu3lIFWnC5lQmZNvOa/ho=', - delegator: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', - validator: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', + from: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', + to: 'osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26', chainId: 'osmo-test-5', accountNumber: 1346, sequence: 3, @@ -191,6 +191,38 @@ export const TEST_TX_WITH_MEMO = { }, }; +export const TEST_EXECUTE_CONTRACT_TRANSACTION = { + hash: '62CA27678BFEFADBE064FFFDA425C9601E6BAD3044F1D2320663BF8594F70719', + signature: 'KexKNBgM2+Uynfdw2IezPrs8J8y1G+J/JHIbPAKQLKANZtCcopig1H5DmlTxAoYeVZ9/b+2WLYJ/FIwvvA993g==', + pubKey: 'ApWuvZlBAfy4KcVaOYNn4T9b6269swAx2QwuEJixkz+W', + privateKey: 'UmzzQXbAKVSR6FTAbmq4xeOvwVXQ/oo60TOju8TFOB0=', + signedTxBase64: + 'CqwBCqkBCiQvY29zbXdhc20ud2FzbS52MS5Nc2dFeGVjdXRlQ29udHJhY3QSgAEKK29zbW8xM2swd3JudnpkOW5qYWhqc3lydGZzODhlbjBqNWR6MDc1azhyNmgSP29zbW8xeGo0dnJhOHFzbThxa2h0dHRnMmEyeW44eW1obDh3eWdtbmRhYXE1c2xweG5nc2ZkaHZ1cXo5cmYwMBoQeyJpbmNyZW1lbnQiOnt9fRJnClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECla69mUEB/LgpxVo5g2fhP1vrbr2zADHZDC4QmLGTP5YSBAoCCAEYCxITCg0KBXVvc21vEgQ1MDAwEICJehpAKexKNBgM2+Uynfdw2IezPrs8J8y1G+J/JHIbPAKQLKANZtCcopig1H5DmlTxAoYeVZ9/b+2WLYJ/FIwvvA993g==', + from: 'osmo13k0wrnvzd9njahjsyrtfs88en0j5dz075k8r6h', + to: 'osmo1xj4vra8qsm8qkhtttg2a2yn8ymhl8wygmndaaq5slpxngsfdhvuqz9rf00', + chainId: 'osmo-test-5', + accountNumber: 2392, + sequence: 11, + feeAmount: '5000', + message: { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: { + sender: 'osmo13k0wrnvzd9njahjsyrtfs88en0j5dz075k8r6h', + contract: 'osmo1xj4vra8qsm8qkhtttg2a2yn8ymhl8wygmndaaq5slpxngsfdhvuqz9rf00', + msg: new Uint8Array(Buffer.from('7b22696e6372656d656e74223a7b7d7d', 'hex')), + }, + }, + gasBudget: { + amount: [ + { + denom: 'uosmo', + amount: '5000', + }, + ], + gasLimit: 2000000, + }, +}; + export const address = { address1: 'osmo1jj65dw2qun4rdx6wuvtc4u8wadhhzwmu7zvlw3', address2: 'osmo1memtsmlz95gf938agf5q6qfhhht4ppe8mejcus', diff --git a/modules/sdk-coin-osmo/test/unit/osmo.ts b/modules/sdk-coin-osmo/test/unit/osmo.ts index c4b09ebff4..4d9a5a98a7 100644 --- a/modules/sdk-coin-osmo/test/unit/osmo.ts +++ b/modules/sdk-coin-osmo/test/unit/osmo.ts @@ -10,6 +10,7 @@ import { TEST_TX_WITH_MEMO, TEST_UNDELEGATE_TX, TEST_WITHDRAW_REWARDS_TX, + TEST_EXECUTE_CONTRACT_TRANSACTION, address, } from '../resources/osmo'; import should = require('should'); @@ -107,7 +108,7 @@ describe('OSMO', function () { const txParams = { recipients: [ { - address: TEST_SEND_TX.recipient, + address: TEST_SEND_TX.to, amount: TEST_SEND_TX.sendAmount, }, ], @@ -125,7 +126,7 @@ describe('OSMO', function () { const txParams = { recipients: [ { - address: TEST_DELEGATE_TX.validator, + address: TEST_DELEGATE_TX.to, amount: TEST_DELEGATE_TX.sendAmount, }, ], @@ -143,7 +144,7 @@ describe('OSMO', function () { const txParams = { recipients: [ { - address: TEST_UNDELEGATE_TX.validator, + address: TEST_UNDELEGATE_TX.to, amount: TEST_UNDELEGATE_TX.sendAmount, }, ], @@ -161,7 +162,25 @@ describe('OSMO', function () { const txParams = { recipients: [ { - address: TEST_WITHDRAW_REWARDS_TX.validator, + address: TEST_WITHDRAW_REWARDS_TX.to, + amount: 'UNAVAILABLE', + }, + ], + }; + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should succeed to verify execute contract transaction', async function () { + const txPrebuild = { + txHex: TEST_EXECUTE_CONTRACT_TRANSACTION.signedTxBase64, + txInfo: {}, + }; + const txParams = { + recipients: [ + { + address: TEST_EXECUTE_CONTRACT_TRANSACTION.to, amount: 'UNAVAILABLE', }, ], @@ -193,7 +212,7 @@ describe('OSMO', function () { id: TEST_SEND_TX.hash, outputs: [ { - address: TEST_SEND_TX.recipient, + address: TEST_SEND_TX.to, amount: TEST_SEND_TX.sendAmount, }, ], @@ -214,7 +233,7 @@ describe('OSMO', function () { id: TEST_DELEGATE_TX.hash, outputs: [ { - address: TEST_DELEGATE_TX.validator, + address: TEST_DELEGATE_TX.to, amount: TEST_DELEGATE_TX.sendAmount, }, ], @@ -235,7 +254,7 @@ describe('OSMO', function () { id: TEST_UNDELEGATE_TX.hash, outputs: [ { - address: TEST_UNDELEGATE_TX.validator, + address: TEST_UNDELEGATE_TX.to, amount: TEST_UNDELEGATE_TX.sendAmount, }, ], @@ -256,11 +275,11 @@ describe('OSMO', function () { id: TEST_WITHDRAW_REWARDS_TX.hash, outputs: [ { - address: TEST_WITHDRAW_REWARDS_TX.validator, + address: TEST_WITHDRAW_REWARDS_TX.to, amount: 'UNAVAILABLE', }, ], - outputAmount: undefined, + outputAmount: 'UNAVAILABLE', changeOutputs: [], changeAmount: '0', fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount }, @@ -268,6 +287,27 @@ describe('OSMO', function () { }); }); + it('should explain a execute contract transaction', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_EXECUTE_CONTRACT_TRANSACTION.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_EXECUTE_CONTRACT_TRANSACTION.hash, + outputs: [ + { + address: TEST_EXECUTE_CONTRACT_TRANSACTION.to, + amount: 'UNAVAILABLE', + }, + ], + outputAmount: 'UNAVAILABLE', + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_EXECUTE_CONTRACT_TRANSACTION.gasBudget.amount[0].amount }, + type: 16, + }); + }); + it('should explain a transfer transaction with memo', async function () { const explainedTransaction = await basecoin.explainTransaction({ txHex: TEST_TX_WITH_MEMO.signedTxBase64, @@ -309,12 +349,12 @@ describe('OSMO', function () { describe('Parse Transactions: ', () => { const transferInputsResponse = { - address: TEST_SEND_TX.recipient, + address: TEST_SEND_TX.to, amount: new BigNumber(TEST_SEND_TX.sendAmount).plus(TEST_SEND_TX.gasBudget.amount[0].amount).toFixed(), }; const transferOutputsResponse = { - address: TEST_SEND_TX.recipient, + address: TEST_SEND_TX.to, amount: TEST_SEND_TX.sendAmount, }; diff --git a/modules/sdk-coin-osmo/test/unit/transaction.ts b/modules/sdk-coin-osmo/test/unit/transaction.ts index 21152ba7dc..06c34fbe1c 100644 --- a/modules/sdk-coin-osmo/test/unit/transaction.ts +++ b/modules/sdk-coin-osmo/test/unit/transaction.ts @@ -6,8 +6,10 @@ import should from 'should'; import { CosmosTransaction, DelegateOrUndelegeteMessage, + ExecuteContractMessage, SendMessage, WithdrawDelegatorRewardsMessage, + CosmosConstants, } from '@bitgo/abstract-cosmos'; import utils from '../../src/lib/utils'; import * as testData from '../resources/osmo'; @@ -46,7 +48,7 @@ describe('Osmo Transaction', () => { tx.loadInputsAndOutputs(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_SEND_TX.sender, + address: testData.TEST_SEND_TX.from, value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, coin: 'tosmo', }, @@ -79,7 +81,7 @@ describe('Osmo Transaction', () => { tx.loadInputsAndOutputs(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_SEND_TX.sender, + address: testData.TEST_SEND_TX.from, value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, coin: 'tosmo', }, @@ -112,14 +114,14 @@ describe('Osmo Transaction', () => { tx.loadInputsAndOutputs(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_DELEGATE_TX.delegator, + address: testData.TEST_DELEGATE_TX.from, value: testData.TEST_DELEGATE_TX.sendMessage.value.amount.amount, coin: 'tosmo', }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_DELEGATE_TX.validator, + address: testData.TEST_DELEGATE_TX.to, value: testData.TEST_DELEGATE_TX.sendMessage.value.amount.amount, coin: 'tosmo', }, @@ -145,14 +147,14 @@ describe('Osmo Transaction', () => { tx.loadInputsAndOutputs(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_UNDELEGATE_TX.delegator, + address: testData.TEST_UNDELEGATE_TX.from, value: testData.TEST_UNDELEGATE_TX.sendMessage.value.amount.amount, coin: 'tosmo', }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_UNDELEGATE_TX.validator, + address: testData.TEST_UNDELEGATE_TX.to, value: testData.TEST_UNDELEGATE_TX.sendMessage.value.amount.amount, coin: 'tosmo', }, @@ -178,14 +180,50 @@ describe('Osmo Transaction', () => { tx.loadInputsAndOutputs(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + address: testData.TEST_WITHDRAW_REWARDS_TX.from, value: 'UNAVAILABLE', coin: 'tosmo', }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + address: testData.TEST_WITHDRAW_REWARDS_TX.to, + value: 'UNAVAILABLE', + coin: 'tosmo', + }, + ]); + }); + + it('should build a execute contract from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_EXECUTE_CONTRACT_TRANSACTION.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_EXECUTE_CONTRACT_TRANSACTION.sequence); + should.deepEqual(json.gasBudget, testData.TEST_EXECUTE_CONTRACT_TRANSACTION.gasBudget); + should.equal( + Buffer.from(json.publicKey as any, 'hex').toString('base64'), + testData.TEST_EXECUTE_CONTRACT_TRANSACTION.pubKey + ); + should.equal( + (json.sendMessages[0].value as ExecuteContractMessage).contract, + testData.TEST_EXECUTE_CONTRACT_TRANSACTION.message.value.contract + ); + should.equal( + Buffer.from(json.signature as any).toString('base64'), + testData.TEST_EXECUTE_CONTRACT_TRANSACTION.signature + ); + should.equal(tx.type, TransactionType.ContractCall); + + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_EXECUTE_CONTRACT_TRANSACTION.from, + value: 'UNAVAILABLE', + coin: 'tosmo', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_EXECUTE_CONTRACT_TRANSACTION.to, value: 'UNAVAILABLE', coin: 'tosmo', }, @@ -213,7 +251,7 @@ describe('Osmo Transaction', () => { id: testData.TEST_SEND_TX.hash, outputs: [ { - address: testData.TEST_SEND_TX.recipient, + address: testData.TEST_SEND_TX.to, amount: testData.TEST_SEND_TX.sendAmount, }, ], @@ -225,6 +263,26 @@ describe('Osmo Transaction', () => { }); }); + it('should explain a execute contract transaction', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_EXECUTE_CONTRACT_TRANSACTION.signedTxBase64); + const explainedTransaction = tx.explainTransaction(); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: testData.TEST_EXECUTE_CONTRACT_TRANSACTION.hash, + outputs: [ + { + address: testData.TEST_EXECUTE_CONTRACT_TRANSACTION.to, + amount: CosmosConstants.UNAVAILABLE_TEXT, + }, + ], + outputAmount: CosmosConstants.UNAVAILABLE_TEXT, + changeOutputs: [], + changeAmount: '0', + fee: { fee: testData.TEST_EXECUTE_CONTRACT_TRANSACTION.feeAmount }, + type: 16, + }); + }); + it('should fail to explain transaction with invalid raw base64 string', function () { should.throws(() => tx.enrichTransactionDetailsFromRawTransaction('randomString'), 'Invalid transaction'); }); diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/ContractCallBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/ContractCallBuilder.ts new file mode 100644 index 0000000000..f0a33930f5 --- /dev/null +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/ContractCallBuilder.ts @@ -0,0 +1,121 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import * as testData from '../../resources/osmo'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Osmo, Tosmo } from '../../../src'; + +describe('Osmo contract call txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('osmo', Osmo.createInstance); + bitgo.safeRegister('tosmo', Tosmo.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tosmo'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_EXECUTE_CONTRACT_TRANSACTION; + }); + + it('should build a contractCall tx with signature', async function () { + const txBuilder = factory.getContractCallBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.message.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.ContractCall); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.message]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.from, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.to, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a contractCall tx without signature', async function () { + const txBuilder = factory.getContractCallBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.message.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.ContractCall); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.message]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testTx.from, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.to, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign a contract call tx', async function () { + const txBuilder = factory.getContractCallBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.message.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.ContractCall); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.message]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.from, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.to, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingActivateBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingActivateBuilder.ts index 2a6996f20a..9f3497efb0 100644 --- a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingActivateBuilder.ts +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingActivateBuilder.ts @@ -41,14 +41,14 @@ describe('Osmo Delegate txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, @@ -71,14 +71,14 @@ describe('Osmo Delegate txn Builder', () => { tx.toBroadcastFormat(); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, @@ -105,14 +105,14 @@ describe('Osmo Delegate txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingDeactivateBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingDeactivateBuilder.ts index 8f467b8347..6bffea58b9 100644 --- a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingDeactivateBuilder.ts +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingDeactivateBuilder.ts @@ -39,14 +39,14 @@ describe('Osmo Undelegate txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, @@ -69,14 +69,14 @@ describe('Osmo Undelegate txn Builder', () => { tx.toBroadcastFormat(); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, @@ -103,14 +103,14 @@ describe('Osmo Undelegate txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testTx.delegator, + address: testTx.from, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testTx.validator, + address: testTx.to, value: testTx.sendMessage.value.amount.amount, coin: basecoin.getChain(), }, diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts index 0b4b34db90..66749d905f 100644 --- a/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts @@ -41,14 +41,14 @@ describe('Osmo WithdrawRewards txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + address: testData.TEST_WITHDRAW_REWARDS_TX.from, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + address: testData.TEST_WITHDRAW_REWARDS_TX.to, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, @@ -71,14 +71,14 @@ describe('Osmo WithdrawRewards txn Builder', () => { tx.toBroadcastFormat(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + address: testData.TEST_WITHDRAW_REWARDS_TX.from, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + address: testData.TEST_WITHDRAW_REWARDS_TX.to, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, @@ -105,14 +105,14 @@ describe('Osmo WithdrawRewards txn Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + address: testData.TEST_WITHDRAW_REWARDS_TX.from, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, ]); should.deepEqual(tx.outputs, [ { - address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + address: testData.TEST_WITHDRAW_REWARDS_TX.to, value: 'UNAVAILABLE', coin: basecoin.getChain(), }, diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/transactionBuilder.ts index c3e92b81bc..32509b4d3e 100644 --- a/modules/sdk-coin-osmo/test/unit/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/transactionBuilder.ts @@ -10,6 +10,7 @@ describe('Osmo Transaction Builder', async () => { let bitgo: TestBitGoAPI; let basecoin; let factory; + let data; before(function () { bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); bitgo.safeRegister('osmo', Osmo.createInstance); @@ -17,12 +18,6 @@ describe('Osmo Transaction Builder', async () => { bitgo.initializeTestVars(); basecoin = bitgo.coin('tosmo'); factory = basecoin.getBuilder(); - }); - - const testTxData = testData.TEST_SEND_TX; - let data; - - beforeEach(() => { data = [ { type: TransactionType.Send, @@ -44,37 +39,41 @@ describe('Osmo Transaction Builder', async () => { testTx: testData.TEST_WITHDRAW_REWARDS_TX, builder: factory.getStakingWithdrawRewardsBuilder(), }, + { + type: TransactionType.ContractCall, + testTx: testData.TEST_EXECUTE_CONTRACT_TRANSACTION, + builder: factory.getContractCallBuilder(), + }, ]; }); it('should build a signed tx from signed tx data', async function () { - const txBuilder = factory.from(testTxData.signedTxBase64); - const tx = await txBuilder.build(); - should.equal(tx.type, TransactionType.Send); - // Should recreate the same raw tx data when re-build and turned to broadcast format - const rawTx = tx.toBroadcastFormat(); - should.equal(rawTx, testTxData.signedTxBase64); + for (const { type, testTx } of data) { + const txBuilder = factory.from(testTx.signedTxBase64); + const tx = await txBuilder.build(); + should.equal(tx.type, type); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + } }); - describe('gasBudget tests', async () => { - it('should succeed for valid gasBudget', function () { - for (const { builder } of data) { - should.doesNotThrow(() => builder.gasBudget(testTxData.gasBudget)); - } - }); + it('should succeed for valid gasBudget', function () { + for (const { testTx, builder } of data) { + should.doesNotThrow(() => builder.gasBudget(testTx.gasBudget)); + } + }); - it('should throw for invalid gasBudget', function () { - const invalidGasBudget = 0; - for (const { builder } of data) { - should(() => builder.gasBudget({ gasLimit: invalidGasBudget })).throw('Invalid gas limit ' + invalidGasBudget); - } - }); + it('should throw for invalid gasBudget', function () { + const invalidGasBudget = 0; + for (const { builder } of data) { + should(() => builder.gasBudget({ gasLimit: invalidGasBudget })).throw('Invalid gas limit ' + invalidGasBudget); + } }); it('validateAddress', function () { const invalidAddress = { address: 'randomString' }; - for (const { builder } of data) { - should.doesNotThrow(() => builder.validateAddress({ address: testTxData.sender })); + for (const { testTx, builder } of data) { + should.doesNotThrow(() => builder.validateAddress({ address: testTx.from })); should(() => builder.validateAddress(invalidAddress)).throwError( 'transactionBuilder: address isValidAddress check failed: ' + invalidAddress.address ); diff --git a/modules/sdk-coin-osmo/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-osmo/test/unit/transactionBuilder/transferBuilder.ts index 5e189fa782..b24aef91cf 100644 --- a/modules/sdk-coin-osmo/test/unit/transactionBuilder/transferBuilder.ts +++ b/modules/sdk-coin-osmo/test/unit/transactionBuilder/transferBuilder.ts @@ -42,7 +42,7 @@ describe('Osmo Transfer Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testData.TEST_SEND_TX.sender, + address: testData.TEST_SEND_TX.from, value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, coin: basecoin.getChain(), }, @@ -110,7 +110,7 @@ describe('Osmo Transfer Builder', () => { tx.toBroadcastFormat(); should.deepEqual(tx.inputs, [ { - address: testData.TEST_SEND_TX.sender, + address: testData.TEST_SEND_TX.from, value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, coin: basecoin.getChain(), }, @@ -144,7 +144,7 @@ describe('Osmo Transfer Builder', () => { should.equal(rawTx, testTx.signedTxBase64); should.deepEqual(tx.inputs, [ { - address: testData.TEST_SEND_TX.sender, + address: testData.TEST_SEND_TX.from, value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, coin: basecoin.getChain(), }, diff --git a/modules/sdk-coin-sei/src/lib/constants.ts b/modules/sdk-coin-sei/src/lib/constants.ts index 35630daff0..bcdec98b99 100644 --- a/modules/sdk-coin-sei/src/lib/constants.ts +++ b/modules/sdk-coin-sei/src/lib/constants.ts @@ -1,3 +1,4 @@ export const validDenoms = ['nsei', 'usei', 'msei', 'sei']; -export const accountAddressRegex = /^(sei)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; -export const validatorAddressRegex = /^(seivaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; +export const accountAddressRegex = /^(sei)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const validatorAddressRegex = /^(seivaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const contractAddressRegex = /^(sei)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; diff --git a/modules/sdk-coin-sei/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-sei/src/lib/transactionBuilderFactory.ts index 00c9aed430..43f9c5b43f 100644 --- a/modules/sdk-coin-sei/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-sei/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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'); } @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, seiUtils)); } + getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder { + return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, seiUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-sei/src/lib/utils.ts b/modules/sdk-coin-sei/src/lib/utils.ts index 674708e28e..18ecbad59e 100644 --- a/modules/sdk-coin-sei/src/lib/utils.ts +++ b/modules/sdk-coin-sei/src/lib/utils.ts @@ -16,6 +16,11 @@ export class SeiUtils 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); diff --git a/modules/sdk-coin-sei/test/unit/sei.ts b/modules/sdk-coin-sei/test/unit/sei.ts index 7a7ea484ee..4f0411b1cf 100644 --- a/modules/sdk-coin-sei/test/unit/sei.ts +++ b/modules/sdk-coin-sei/test/unit/sei.ts @@ -260,7 +260,7 @@ describe('SEI', function () { amount: 'UNAVAILABLE', }, ], - outputAmount: undefined, + outputAmount: 'UNAVAILABLE', changeOutputs: [], changeAmount: '0', fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount }, diff --git a/modules/sdk-coin-tia/src/lib/constants.ts b/modules/sdk-coin-tia/src/lib/constants.ts index 2d1c3319ab..24ce81324e 100644 --- a/modules/sdk-coin-tia/src/lib/constants.ts +++ b/modules/sdk-coin-tia/src/lib/constants.ts @@ -1,3 +1,4 @@ export const validDenoms = ['ntia', 'utia', 'mtia', 'tia']; -export const accountAddressRegex = /^(celestia)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; -export const validatorAddressRegex = /^(celestiavaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; +export const accountAddressRegex = /^(celestia)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const validatorAddressRegex = /^(celestiavaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const contractAddressRegex = /^(celestia)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; diff --git a/modules/sdk-coin-tia/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-tia/src/lib/transactionBuilderFactory.ts index df87cc060d..aef690f13c 100644 --- a/modules/sdk-coin-tia/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-tia/src/lib/transactionBuilderFactory.ts @@ -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'; @@ -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'); } @@ -57,6 +60,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, tiaUtils)); } + getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder { + return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, tiaUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-tia/src/lib/utils.ts b/modules/sdk-coin-tia/src/lib/utils.ts index 83572b6ed3..3f506080ef 100644 --- a/modules/sdk-coin-tia/src/lib/utils.ts +++ b/modules/sdk-coin-tia/src/lib/utils.ts @@ -16,6 +16,11 @@ export class TiaUtils 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); diff --git a/modules/sdk-coin-tia/test/unit/tia.ts b/modules/sdk-coin-tia/test/unit/tia.ts index 02b20f091d..07536be1e6 100644 --- a/modules/sdk-coin-tia/test/unit/tia.ts +++ b/modules/sdk-coin-tia/test/unit/tia.ts @@ -260,7 +260,7 @@ describe('TIA', function () { amount: 'UNAVAILABLE', }, ], - outputAmount: undefined, + outputAmount: 'UNAVAILABLE', changeOutputs: [], changeAmount: '0', fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount },