From 173730ad375bf2d484df424e250a39cce55f88c6 Mon Sep 17 00:00:00 2001 From: cdc-Hitesh Date: Tue, 4 Jan 2022 23:45:24 +0530 Subject: [PATCH] #355 MsgCreateVestingAccount support --- lib/src/core/cro.ts | 4 + lib/src/cosmos/v1beta1/types/typeurls.ts | 1 + .../transaction/common/constants/typeurl.ts | 3 + .../account/MsgCreateVestingAccount.spec.ts | 204 ++++++++++++++++++ .../msg/account/MsgCreateVestingAccount.ts | 140 ++++++++++++ lib/src/transaction/msg/ow.types.ts | 8 + 6 files changed, 360 insertions(+) create mode 100644 lib/src/transaction/msg/account/MsgCreateVestingAccount.spec.ts create mode 100644 lib/src/transaction/msg/account/MsgCreateVestingAccount.ts diff --git a/lib/src/core/cro.ts b/lib/src/core/cro.ts index 53f51ead..943f0d21 100644 --- a/lib/src/core/cro.ts +++ b/lib/src/core/cro.ts @@ -46,6 +46,7 @@ import { msgConsensusState } from '../transaction/msg/ibc/lightclients/Consensus import { msgHeader } from '../transaction/msg/ibc/lightclients/Header'; import { MsgConnectionOpenConfirmIBC } from '../transaction/msg/ibc/core/connection/MsgConnectionOpenConfirm'; import { MsgConnectionOpenTryIBC } from '../transaction/msg/ibc/core/connection/MsgConnectionOpenTry'; +import { msgCreateVestingAccount } from '../transaction/msg/account/MsgCreateVestingAccount'; export const CroSDK = function (configs: InitConfigurations) { ow(configs, 'configs', owCroSDKInitParams); @@ -90,6 +91,9 @@ export const CroSDK = function (configs: InitConfigurations) { MsgTransferNFT: msgTransferNFT(configs), MsgBurnNFT: msgBurnNFT(configs), }, + accounts: { + MsgCreateVestingAccount: msgCreateVestingAccount(configs), + }, ibc: { MsgTransfer: msgTransferIBC(configs), MsgCreateClient: msgCreateClientIBC(configs), diff --git a/lib/src/cosmos/v1beta1/types/typeurls.ts b/lib/src/cosmos/v1beta1/types/typeurls.ts index 1b7cba3c..bb902f3f 100644 --- a/lib/src/cosmos/v1beta1/types/typeurls.ts +++ b/lib/src/cosmos/v1beta1/types/typeurls.ts @@ -60,6 +60,7 @@ export const typeUrlMappings: { '/ibc.lightclients.tendermint.v1.ClientState': ibc.lightclients.tendermint.v1.ClientState, '/ibc.lightclients.tendermint.v1.ConsensusState': ibc.lightclients.tendermint.v1.ConsensusState, '/ibc.lightclients.tendermint.v1.Header': ibc.lightclients.tendermint.v1.Header, + '/cosmos.vesting.v1beta1.MsgCreateVestingAccount': cosmos.vesting.v1beta1.MsgCreateVestingAccount, }; export interface GeneratedType { diff --git a/lib/src/transaction/common/constants/typeurl.ts b/lib/src/transaction/common/constants/typeurl.ts index 5904dcd5..dd32916a 100644 --- a/lib/src/transaction/common/constants/typeurl.ts +++ b/lib/src/transaction/common/constants/typeurl.ts @@ -50,6 +50,9 @@ export const COSMOS_MSG_TYPEURL = { MsgConnectionOpenTry: '/ibc.core.connection.v1.MsgConnectionOpenTry', }, }, + account: { + MsgCreateVestingAccount: '/cosmos.vesting.v1beta1.MsgCreateVestingAccount', + }, }; export const typeUrlToMsgClassMapping = (cro: any, typeUrl: string) => { diff --git a/lib/src/transaction/msg/account/MsgCreateVestingAccount.spec.ts b/lib/src/transaction/msg/account/MsgCreateVestingAccount.spec.ts new file mode 100644 index 00000000..a2e312af --- /dev/null +++ b/lib/src/transaction/msg/account/MsgCreateVestingAccount.spec.ts @@ -0,0 +1,204 @@ +import 'mocha'; +import { expect } from 'chai'; +import Big from 'big.js'; + +import Long from 'long'; +import { fuzzyDescribe } from '../../../test/mocha-fuzzy/suite'; +import { Msg } from '../../../cosmos/v1beta1/types/msg'; +import { Secp256k1KeyPair } from '../../../keypair/secp256k1'; +import { Bytes } from '../../../utils/bytes/bytes'; +import { Units } from '../../../coin/coin'; +import { CroSDK } from '../../../core/cro'; + +const cro = CroSDK({ + network: { + defaultNodeUrl: '', + chainId: 'testnet-croeseid-1', + addressPrefix: 'tcro', + validatorAddressPrefix: 'tcrocncl', + validatorPubKeyPrefix: 'tcrocnclconspub', + coin: { + baseDenom: 'basetcro', + croDenom: 'tcro', + }, + bip44Path: { + coinType: 1, + account: 0, + }, + rpcUrl: '', + }, +}); + +describe('Testing MsgCreateVestingAccount', function () { + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a MsgCreateVestingAccount', function () { + const json = + '{ "@type": "/cosmos.bank.v1beta1.MsgCreateValidator", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3" }'; + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Expected /cosmos.vesting.v1beta1.MsgCreateVestingAccount but got /cosmos.bank.v1beta1.MsgCreateValidator', + ); + }); + it('should throw Error when the from field is missing', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3", "end_time": "1000000", "delayed": true }'; + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Expected property `fromAddress` to be of type `string` but received type `undefined` in object `options`', + ); + }); + it('should throw Error when the to field is missing', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "end_time": "1000000", "delayed": true }'; + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Expected property `toAddress` to be of type `string` but received type `undefined` in object `options`', + ); + }); + it('should throw Error when the amount field is missing', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg" , "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3", "end_time": "1000000", "delayed": true }'; + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Invalid amount in the Msg.', + ); + }); + it('should throw on invalid `fromAddress`', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "from_address": "cro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8f", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3", "end_time": "1000000", "delayed": true }'; + + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Provided `fromAddress` does not match network selected', + ); + }); + it('should throw on invalid `toAddress`', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "from_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3", "to_address": "cro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8f", "end_time": "1000000", "delayed": true }'; + + expect(() => cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json)).to.throw( + 'Provided `toAddress` does not match network selected', + ); + }); + it('should return the MsgCreateVestingAccount corresponding to the JSON', function () { + const json = + '{ "@type": "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", "amount": { "denom": "basetcro", "amount": "3478499933290496" }, "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3", "end_time": "1000000", "delayed": true }'; + const MsgCreateVestingAccount = cro.accounts.MsgCreateVestingAccount.fromCosmosMsgJSON(json); + expect(MsgCreateVestingAccount.fromAddress).to.eql('tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg'); + expect(MsgCreateVestingAccount.toAddress).to.eql('tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3'); + expect(MsgCreateVestingAccount.amount.toCosmosCoin().amount).to.eql('3478499933290496'); + expect(MsgCreateVestingAccount.amount.toCosmosCoin().denom).to.eql('basetcro'); + }); + }); + + fuzzyDescribe('should throw Error when options is invalid', function (fuzzy) { + const anyValidOptions = { + fromAddress: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + toAddress: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: new cro.Coin('1000', Units.BASE), + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidOptions)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.accounts.MsgCreateVestingAccount(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + it('Test MsgCreateVestingAccount conversion', function () { + const coin = new cro.Coin('12000500', Units.BASE); + + const MsgCreateVestingAccount = new cro.accounts.MsgCreateVestingAccount({ + fromAddress: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + toAddress: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: coin, + delayed: true, + endTime: Long.fromString('1000000'), + }); + + const rawMsg: Msg = { + typeUrl: '/cosmos.vesting.v1beta1.MsgCreateVestingAccount', + value: { + fromAddress: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + toAddress: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: { + denom: 'basetcro', + amount: '12000500', + }, + delayed: true, + endTime: Long.fromString('1000000'), + }, + }; + + expect(MsgCreateVestingAccount.toRawMsg()).to.eqls(rawMsg); + }); + + it('Test appendTxBody MsgCreateVestingAccount Tx signing', function () { + const anyKeyPair = Secp256k1KeyPair.fromPrivKey( + Bytes.fromHexString('66633d18513bec30dd11a209f1ceb1787aa9e2069d5d47e590174dc9665102b3'), + ); + const coin = new cro.Coin('12000500', Units.CRO); + + const MsgCreateVestingAccount = new cro.accounts.MsgCreateVestingAccount({ + fromAddress: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + toAddress: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: coin, + delayed: true, + endTime: Long.fromString('1000000'), + }); + + const anySigner = { + publicKey: anyKeyPair.getPubKey(), + accountNumber: new Big(0), + accountSequence: new Big(2), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(MsgCreateVestingAccount).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, anyKeyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.encode().toHexString(); + expect(signedTxHex).to.be.eql( + '0a96010a93010a2f2f636f736d6f732e76657374696e672e763162657461312e4d736743726561746556657374696e674163636f756e7412600a2b7463726f313635747a63726832796c3833673871657178756567326735677a6775353779336665336b6333122b7463726f3138346c7461326c7379753437767779703265387a6d746361336b35797138357036633476703320c0843d280112580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103fd0d560b6c4aa1ca16721d039a192867c3457e19dad553edb98e7ba88b159c2712040a0208011802120410c09a0c1a40df1e553c5bb66a2697186cb717d04ba997f0c921de36df1b50e51a1ae05c1e9d42693bad19336625b75ff1087ebf58e5f532ce103b2986a581e462dc5d59ced4', + ); + }); + + it('Should validate MsgCreateVestingAccount provided addresses with network config', function () { + const coin = new cro.Coin('12000500', Units.BASE); + + const params1 = { + fromAddress: 'cro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8f', + toAddress: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: coin, + delayed: true, + endTime: Long.fromString('1000000'), + }; + + const params2 = { + fromAddress: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + toAddress: 'cro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8f', + amount: coin, + delayed: true, + endTime: Long.fromString('1000000'), + }; + + const params3 = { + fromAddress: 'tcro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8fzqa', + toAddress: 'cro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: coin, + delayed: true, + endTime: Long.fromString('1000000'), + }; + + expect(() => new cro.accounts.MsgCreateVestingAccount(params1)).to.throw( + 'Provided `fromAddress` does not match network selected', + ); + expect(() => new cro.accounts.MsgCreateVestingAccount(params2)).to.throw( + 'Provided `toAddress` does not match network selected', + ); + expect(() => new cro.accounts.MsgCreateVestingAccount(params3)).to.throw( + 'Invalid checksum for tcro1pndm4ywdf4qtmupa0fqe75krmqed2znjyj6x8fzqa', + ); + }); +}); diff --git a/lib/src/transaction/msg/account/MsgCreateVestingAccount.ts b/lib/src/transaction/msg/account/MsgCreateVestingAccount.ts new file mode 100644 index 00000000..34b76723 --- /dev/null +++ b/lib/src/transaction/msg/account/MsgCreateVestingAccount.ts @@ -0,0 +1,140 @@ +/* eslint-disable camelcase */ +import ow from 'ow'; +import Long from 'long'; +import { Msg } from '../../../cosmos/v1beta1/types/msg'; +import { owMsgCreateVestingAccountOptions } from '../ow.types'; +import { InitConfigurations, CroSDK } from '../../../core/cro'; +import { AddressType, validateAddress } from '../../../utils/address'; +import { CosmosMsg } from '../cosmosMsg'; +import { COSMOS_MSG_TYPEURL } from '../../common/constants/typeurl'; +import * as legacyAmino from '../../../cosmos/amino'; +import { ICoin } from '../../../coin/v2.coin/v2.coin'; + +export interface MsgCreateVestingAccountRaw { + '@type': string; + amount: Amount; + from_address: string; + to_address: string; + end_time: string; + delayed: boolean; +} + +export interface Amount { + denom: string; + amount: string; +} + +export const msgCreateVestingAccount = function (config: InitConfigurations) { + return class MsgCreateVestingAccount implements CosmosMsg { + /** MsgCreateVestingAccount fromAddress. */ + public fromAddress: string; + + /** MsgCreateVestingAccount toAddress. */ + public toAddress: string; + + /** MsgCreateVestingAccount amount. */ + public amount: ICoin; + + /** MsgCreateVestingAccount endTime. */ + public endTime: Long; + + /** MsgCreateVestingAccount delayed. */ + public delayed: boolean; + + /** + * Constructor to create a new MsgCreateVestingAccount + * @param {MsgCreateVestingAccountOptions} options + * @returns {MsgCreateVestingAccount} + * @throws {Error} when options is invalid + */ + constructor(options: MsgCreateVestingAccountOptions) { + ow(options, 'options', owMsgCreateVestingAccountOptions); + + this.fromAddress = options.fromAddress; + this.toAddress = options.toAddress; + this.amount = options.amount; + this.endTime = options.endTime; + this.delayed = options.delayed; + this.validateAddresses(); + } + + /** + * Returns an instance of MsgCreateVestingAccount + * @param {string} msgJsonStr + * @param {Network} network + * @returns {MsgCreateVestingAccount} + */ + public static fromCosmosMsgJSON(msgJsonStr: string): MsgCreateVestingAccount { + const parsedMsg = JSON.parse(msgJsonStr) as MsgCreateVestingAccountRaw; + const cro = CroSDK({ network: config.network }); + if (parsedMsg['@type'] !== COSMOS_MSG_TYPEURL.account.MsgCreateVestingAccount) { + throw new Error( + `Expected ${COSMOS_MSG_TYPEURL.account.MsgCreateVestingAccount} but got ${parsedMsg['@type']}`, + ); + } + if (!parsedMsg.amount) { + throw new Error('Invalid amount in the Msg.'); + } + + return new MsgCreateVestingAccount({ + fromAddress: parsedMsg.from_address, + toAddress: parsedMsg.to_address, + amount: cro.v2.CoinV2.fromCustomAmountDenom(parsedMsg.amount.amount, parsedMsg.amount.denom), + endTime: Long.fromString(parsedMsg.end_time), + delayed: parsedMsg.delayed, + }); + } + + /** + * Returns the raw Msg representation of MsgCreateVestingAccount + * @returns {Msg} + */ + toRawMsg(): Msg { + return { + typeUrl: COSMOS_MSG_TYPEURL.account.MsgCreateVestingAccount, + value: { + fromAddress: this.fromAddress, + toAddress: this.toAddress, + amount: this.amount.toCosmosCoin(), + endTime: this.endTime, + delayed: this.delayed, + }, + }; + } + + // eslint-disable-next-line class-methods-use-this + toRawAminoMsg(): legacyAmino.Msg { + throw new Error("MsgCreateVestingAccount isn't supported for Amino encoding."); + } + + validateAddresses() { + if ( + !validateAddress({ + address: this.fromAddress, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `fromAddress` does not match network selected'); + } + + if ( + !validateAddress({ + address: this.toAddress, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `toAddress` does not match network selected'); + } + } + }; +}; + +export type MsgCreateVestingAccountOptions = { + fromAddress: string; + toAddress: string; + amount: ICoin; + endTime: Long; + delayed: boolean; +}; diff --git a/lib/src/transaction/msg/ow.types.ts b/lib/src/transaction/msg/ow.types.ts index 0ab856b1..a708d8ba 100644 --- a/lib/src/transaction/msg/ow.types.ts +++ b/lib/src/transaction/msg/ow.types.ts @@ -438,3 +438,11 @@ export const owMsgConnectionOpenTryOptions = owStrictObject().exactShape({ consensusHeight: ow.any(owIBCHeightOptional(), ow.null), signer: ow.string, }); + +export const owMsgCreateVestingAccountOptions = owStrictObject().exactShape({ + fromAddress: ow.string, + toAddress: ow.string, + amount: owCoin(), + endTime: owLong(), + delayed: ow.boolean, +});